(一)宇宙大战 Space Battle — 新建场景Scene、精灵节点、Particle粒子及背景音乐

开始游戏教程前,首先介绍一下SpriteKit(https://developer.apple.com/spritekit/)是什么?
SpriteKit提供了一个图形渲染和动画的基础结构,你可以使用它让任意类型的纹理图片或者精灵动起来。SpriteKit使用渲染循环,利用图形硬件渲染动画的每一帧。

在iOS传统的view的系统中,view的内容被渲染一次后就将一直等待,直到需要渲染的内容发生改变(比如用户发生交互,view的迁移等)的时候,才进行下一次渲染。这主要是因为传统的view大多工作在静态环境下,并没有需要频繁改变的需求。而对于SpriteKit来说,其本身就是用来制作大多数时候是动态的游戏的,为了保证动画的流畅和场景的持续更新,在SpriteKit中view将会循环不断地重绘。

动画和渲染的进程是和SKScene对象绑定的,只有当场景被呈现时,这些渲染以及其中的action才会被执行。SKScene实例中,一个循环按执行顺序包括:

每一帧开始时,SKScene的-update:方法将被调用,参数是从开始时到调用时所经过的时间。在该方法中,我们应该实现一些游戏逻辑,包括AI,精灵行为等等,另外也可以在该方法中更新node的属性或者让node执行action
在update执行完毕后,SKScene将会开始执行所有的action。因为action是可以由开发者设定的(还记得runBlock:么),因此在这一个阶段我们也是可以执行自己的代码的。

在当前帧的action结束之后,SKScene的-didEvaluateActions将被调用,我们可以在这个方法里对结点做最后的调整或者限制,之后将进入物理引擎的计算阶段。

然后SKScene将会开始物理计算,如果在结点上添加了SKPhysicsBody的话,那么这个结点将会具有物理特性,并参与到这个阶段的计算。根据物理计算的结果,SpriteKit将会决定结点新的状态。

然后-didSimulatePhysics会被调用,这类似之前的-didEvaluateActions。这里是我们开发者能参与的最后的地方,是我们改变结点的最后机会。
一帧的最后是渲染流程,根据之前设定和计算的结果对整个呈现的场景进行绘制。完成之后,SpriteKit将开始新的一帧。

在了解了一些SpriteKit的基础概念后,就跟着iFIERO来创建一个简单的游戏作为开启游戏入门之旅吧。

此《宇宙大战 Space Battle》SpirteKit游戏教程共分为三个章节系列,

(一)宇宙大战 Space Battle — 新建场景Scene、精灵节点、Particle粒子及背景音乐(你正在此处进行学习)

(二)宇宙大战 Space Battle — 无限循环背景Endless、SpriteKit物理碰撞、CoreMotion加速计

(三)宇宙大战 Space Battle — 场景SCENE切换、UserDefaults统计分数、Particle粒子效果

你将在此教程中的三个系列当中学到如下的技能:


/*
 * SpaceBattle 宇宙大战 在此游戏中您将获得如下技能:
 *  1、LaunchScreen       学习如何设置游戏启动画面;
 *  2、Scenes             学习如何切换不同的场景 主菜单+游戏场景+游戏结束场景;
 *  3、Accleroation       利用重力加速度 让飞船左右移动;
 *  4、Endless Background 无限循环背景;
 *  5、Scene Edit         直接使用可见即所得操作;
 *  6、UserDefaults       保存游戏分数、最高分;
 *  7、Random             利用可复用的随机函数生成Enemy;
 *  8、Background Music   如何添加背景音乐;
 *  9、Particle           粒子爆炸特效;
 *
 */

应用以上各项SpriteKit与Swift技能,你开发出来的最终手机游戏的效果为如下所示:

一、教程开始 Getting Started

游戏开始前,请下载本教程的初始项目(http://www.ifiero.com/uploads/SpaceBattle-01Starter.zip)。本游戏是由SpriteKit框架、Swift语言,XCODE开发工具进行开发的。

1、打开XCODE(请用正式版,非Beta版),选择Create a new Xcode project,选择iOS->Game,输入Product Name(这里命名为SpaceBattle),开发语言Language选择Swfit,点击Next,工程即新建完毕

iOS > Game

Product Name 输入工程名 SpaceBattle

2、选择Genrnal面板,因为此Space Battle游戏为竖屏游戏,所以去除勾选Deployment Info -> Device Orientation中的Landscape Left 与Landscape Right,我们不需要横屏效果

3、删除XCode左侧目录中的 Action.sks(暂时没有用到),修改GameScene.swift及GameViewController.swift的相关代码

修改GameScene.swift中的代码

修改GameViewController.swift的代码,scene.size = CGSize(width:1536,height:2048)

二、可视化编辑场景 Introducing the Sprite Kit Visual Editor

1.首先需要编辑场景.sks文件,打开GameScene.sks文件,设置场景的尺寸为iPAD4:3的比例(W:1536,H:2048),并删除场景中的文字。

点击Color面板,可修改Scene的场景背景颜色

2.拖动音乐文件到导航栏navigator->SpaceBattle文件夹,勾选Copy items if needed,Added folders选择Create groups,Add to targets勾选SpaceBattle

拖动音乐文件至工程navigator中

3.拖动游戏工程所需要的图片到Assets.xcassets文件夹

4.资源导入后,左侧的导航栏navigator如下图所示

5.Assets图库中的图片尺寸分为1x,2x,3x,你只需设置1x的图片尺寸大小即可,SpriteKit会自动根据你运行的device设备尺寸(iPhoneX,iPhone,iPhone Plus,iPad)进行相应比例的调整。

非常的棒,你已经学会如何导入Mac电脑中的资源文件(图片、音乐、粒子)到SpaceBattle游戏工程内中了。
那么,现在我们就来学习如何新建SpriteKit精灵节点吧!

6.选择左侧导航栏Navigator的GameScene.sks,直接拖动一个Color Sprite到场景中,选中精灵,设置Position(0,0),修改texture为BG_SpaceBattle_planet(AssetsAssets.xcassets文件夹的名称),并命名精灵节点的名称 Name为bg。

拖动一个Color Sprite到场景中,命名精灵节点的名称 Name为bg

7.现在你可以运行模拟器(XCode -> Product -> Run),看看你的游戏是否正确显示你刚刚建立的精灵节点。

选择device设备

Product->Rub (command+R运行)

Simulator模拟器

棒棒哒! 你已学会了如何在场景中建立精灵节点及如何运行模拟器进行调试!

三、SpriteKit Physics 物理引擎

1.Spritekit提供了一个默认的物理模拟系统,用来模拟真实物理世界,可以使得编程者将注意力从力学碰撞和重力模拟的计算中解放出来,通过简单地代码来实现物理碰撞的模拟,而将注意力集中在更需要花费精力的地方。现在,让我们来学习这个系统的使用吧。

首先需要认识两个类,一个是场景scene的属性类SKPhysicsWorld,这个类基于场景,只能被修改但是不能被创建,这个类负责提供重力和检查碰撞(碰撞需要实现SKPhysicsContactDelegate代理协议),另一个就是SKPhysicsBody类,你可以对你的SKNode节点添加物理体属性,来让他们可以参与物理模拟的相关计算。

SpriteKit Physics 物理引擎属性图表:

属 性 功 能 图示
mass 它决定力是如何影响主体,以及当主体参与碰撞时它有多大的动量,以千克为单位
friction 它决定了物体的光滑程度.取值范围为从0.0(表面光滑,物体滑动很顺畅,就像小冰块似的)到1.0(在表面滑动是,物体会很快停止)
linearDamping 物体的线性阻尼.取值范围为0.0(速度从不衰减)到1.0(速度立即衰减).默认值为0.1该属性被用于模拟水流或者空气的阻力.
angularDamping 物体的角速度阻尼.取值范围为0.0(速度从不衰减)到1.0(速度立即衰减).默认值为0.1该属性被用于模拟水流或者空气的阻力.
restitution 描述了当物理实体从另外一个物体上弹出时,还拥有多少能量.基本上我们称之为”反弹力”.它的取值介于0.0(完全不反弹)到1.0(和物体碰撞反弹是所受的力与刚开始碰撞时的力的大小相同)之间.默认值为0.2
density 物体的密度,以千克每立方米为单位.密度是根据单位体积的质量来定义的.密度越高,体积越大,物体也就会越重.密度的默认值为1.0
affectedByGravity 设置物体是否受重力的影响.所有的物体默认的情况都是受重力影响,但是开发者可以简单的吧这个标记设置为NO,使其不受重力影响.
allowsRotation 设置物体是否受到一个旋转力的影响,默认为YES,如果该值设置为NO,物理体将忽略施加在它身上所有的力
resting 设置物理体是否在休息.物理引擎对于一段时间内没有移动过的物体做了一个优化,把他们标记为”正在休息(resting)”,这样,物理引擎就不需要对它们进行计算了.如果你想要手动的唤醒一个正在休息的物体,简单的把resting设置为NO即可
categoryBitMask 一个16进制数,定义了物体的类别.场景中每一个物理体都可以分配到超过32个不同的种类里面,每个对应位中的值
collisionBitMask 一个16进制数,定义哪种类别的物理体可以与之发生碰撞.当两个物体相关联的时候,就可能发生一个碰撞.这个物体的位相对于其他物体的类别做一个逻辑上的加法操作.如果结果是一个非零的值,则该物体收到碰撞的影响
contactTestBitMask 碰撞后发出通知 didBegin可接收到通知
usesPreciseCollisionDetection
设置物体是否使用更精准的碰撞算法.默认情况下,除非确实有必要,Sprite Kit并不会启动精确的冲突检测,因为这样运行效率更高.但是不启动精确的冲突检测会有一个副作用,如果一个物体移动的非常快(比如一个子弹),它可能会直接穿过其他物体.如果这种情况确实发生了,你就应该尝试启动更精准的冲突检测了
velocity
物理体的速度矢量
angularVelocity
物理体的角速度.角速度是一个围绕着一个轴矢量(0.0,0.0,1.0)的速度,单位是弧度每秒

以上图表感谢简书作者的收集整理:https://www.jianshu.com/p/4046bab3a63d

2.对SpriteKit PhysicsBody类的基础的概念了解后,我们现在就来新建player玩家飞船节点playerNode还有alien外星飞船精灵节点,并设置他们的物理属性。

新建player玩家飞船节点 属性面板中Sprite Name命名为playerNode

编辑GameScene.swift


class GameScene: SKScene,SKPhysicsContactDelegate {
    
    private var playerNode:SKSpriteNode!  /// 玩家 宇宙飞船
    
    override func didMove(to view: SKView) {
        
        physicsWorld.gravity = CGVector(dx: 0, dy: -9.8) /// 建立物理世界 重力向下
        physicsWorld.contactDelegate = self              /// 碰撞接触代理为当前scene (GameScene)
        setupPlayer()
    }
    
    //MARK: - 玩家 宇宙飞船
    func setupPlayer(){
        playerNode = childNode(withName: "playerNode") as! SKSpriteNode
        playerNode.physicsBody = SKPhysicsBody(texture: SKTexture(imageNamed: "Player"), size: SKTexture(imageNamed: "Player").size())
        playerNode.physicsBody?.affectedByGravity = false // 不受物理世界的重力影响
        playerNode.physicsBody?.isDynamic = true
        playerNode.physicsBody?.categoryBitMask    = PhysicsCategory.SpaceShip /// 唯一标识
        playerNode.physicsBody?.collisionBitMask   = PhysicsCategory.None      /// 碰撞后要弹开吗
        playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.Alien     /// 碰撞后发出通知
    }
    
    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
    }
}

随机生成alien精灵节点


//MARK: -  生成随机Alien
    @objc func spawnAlien() {
        // 1 or 2
        let i = Int(CGFloat(arc4random()).truncatingRemainder(dividingBy: 2) + 1)
        
        let imageName = "Enemy0\(i)"
        let alien  = SKSpriteNode(imageNamed: imageName)
        alien.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        alien.zPosition   = 1
        alien.name = "Alien"
        var xPosition:CGFloat = 0.0
        // 生成随机的x-Axis轴的位置
        xPosition = CGFloat.random(min: -self.frame.size.width+alien.size.width, max: self.frame.size.width - alien.size.width)
        alien.position = CGPoint(x: xPosition, y: self.frame.size.height + alien.size.height * 2)
        self.addChild(alien)
        // 物理体 PhysicsBody
        alien.physicsBody = SKPhysicsBody(circleOfRadius: alien.size.width / 2)  /// 设置物理身体
        alien.physicsBody?.affectedByGravity = false /// 不受重力影响,自定义飞船移动速度;
        alien.physicsBody?.categoryBitMask   = PhysicsCategory.Alien /// 1.设置唯一属性
        alien.physicsBody?.contactTestBitMask = PhysicsCategory.BulletBlue | PhysicsCategory.SpaceShip /// 2.和哪些节点Node发生碰撞后发出通知
        alien.physicsBody?.collisionBitMask   = PhysicsCategory.None /// 3.碰撞后是否弹开
        
        let duration   = CGFloat.random(min: CGFloat(1.0), max: CGFloat(3.8))  ///随机函数 返回二个数之间的随机数
        let actionDown = SKAction.move(to: CGPoint(x: xPosition, y: -self.frame.size.height), duration: TimeInterval(duration))
        alien.run(SKAction.sequence([actionDown,
                                     SKAction.run({
                                        alien.removeFromParent() // 移除节点;
                                     })]))
        
    }

CGFloat.random 拓展函数 返回二个数之间的随机数


mport CoreGraphics
import SpriteKit

public extension CGFloat {
    
    #if !(arch(x86_64) || arch(arm64))
    func sqrt(a: CGFloat) -> CGFloat {
    return CGFloat(sqrtf(Float(a)))
    }
    #endif
    
    public static func random() -> CGFloat {
        return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
    }
    
    public static func random(min: CGFloat, max: CGFloat) -> CGFloat {
        assert(min < max)
        return CGFloat.random() * (max - min) + min
    }
    
}

在override func didMove(to view: SKView) {}内应用Timer.scheduledTimer间隔0.5秒生成Alien


// spawnAlien()
        Timer.scheduledTimer(timeInterval: TimeInterval(0.5), target: self, selector: #selector(GameScene.spawnAlien), userInfo: nil, repeats: true)

现在Command+R 运行工程下(或选择XCODE -> Product-> Run),你在模拟器中应可以看到源源不断的alien外星飞船正向下俯冲

3.生成子弹及粒子效果


 // MARK: - 生成子弹; 点击屏幕后才发射
    func spawnBulletAndFire(){
        /// 子弹
        let bulletNode = SKSpriteNode(imageNamed: "BulletBlue")
        bulletNode.position.x = playerNode.position.x
        // 子弹的Y轴位置 因为playNode的AnchorPoit位于飞船中心 所以子弹发射时的瞬间位置位于飞船正中心,要加上飞船的半径,位于枪口;
        bulletNode.position.y = playerNode.position.y + playerNode.size.height / 2
        bulletNode.zPosition = 1
        self.addChild(bulletNode)
        bulletNode.physicsBody = SKPhysicsBody(circleOfRadius: bulletNode.size.width / 2)
        bulletNode.physicsBody?.affectedByGravity = false // 子弹不受重力影响;
        bulletNode.physicsBody?.categoryBitMask   =  PhysicsCategory.BulletBlue
        bulletNode.physicsBody?.contactTestBitMask = PhysicsCategory.Alien
        bulletNode.physicsBody?.collisionBitMask  = PhysicsCategory.None
        bulletNode.physicsBody?.usesPreciseCollisionDetection = true ///子弹飞速运动,设置探测精细碰撞
        
        /// 把子弹往上移出屏幕
        let moveTo = CGPoint(x: playerNode.position.x, y: playerNode.position.y + self.frame.size.height)
        
        /*
         * 粒子效果
         * 1.新建一个SKNODE => trailNode
         * 2.新建粒子效果SKEmitterNode,设置tragetNode = trailNode
         * 3.子弹加上emitterNode
         */
        let trailNode = SKNode()
        trailNode.zPosition = 1
        trailNode.name = "trail"
        addChild(trailNode)
        
        let emitterNode = SKEmitterNode(fileNamed: "ShootTrailBlue")! // particles文件夹存放粒子效果
        emitterNode.targetNode = trailNode  /// 设置粒子效果的目标为trailNode => 跟随新建的trailNode
        bulletNode.addChild(emitterNode)    /// 在子弹节点Node加上粒子效果;
        
        bulletNode.run(SKAction.sequence([
            SKAction.move(to: moveTo, duration: TimeInterval(0.5)),
            SKAction.run({
                bulletNode.removeFromParent() /// 移除 子弹bulltedNode
                trailNode.removeFromParent()  /// 移除 trailNode
            })]))
    }

我们将在第二章节学习飞船子弹的发射以及粒子效果的知识

四、到此,此章节就接近尾声了

我们已经学会了很多技能,包括如何新建工程,如何建立Sprite精灵节点,还有如何应用SpriteKit Physics物理引擎。你可以在此下载此章节的工程完整代码。(http://www.iFIERO.com/uploads/SpaceBattle-01final.zip)

五、更多内容

在下一章节当中,(二)宇宙大战 Space Battle — 创建无限循环的背景Endless、监测精灵之间的物体碰撞及物理引擎Accleroation,我们将学习如何监测SpriteKit Physics物理之间碰撞,如何销毁对象,如何监测屏幕Scene的点击事件以及物理引擎Accleroation的相关知识。

请注意,此《宇宙大战 Space Battle》教程共分为三个章节系列:

(一)宇宙大战 Space Battle — 新建场景Scene、导入各个SpriteNode精灵、Particle粒子节点及建立背景音乐(你正在此处进行学习)

(二)宇宙大战 Space Battle — 无限循环背景Endless、物理碰撞、CoreMotion加速

(三)宇宙大战 Space Battle — 各个场景SCENE之间的切换、利用UserDefaults统计分数

打赏

Leave a Reply