iFIERO -- (一) 宇宙大战 SPACE BATTLE — 新建场景SCENE、精灵节点、PARTICLE粒子及背景音乐

开始游戏教程前,首先介绍一下SpriteKit是什么?
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》教程共分为三个章节系列,

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

(二)宇宙大战 Space Battle — 创建无限循环的背景Endless、监测精灵之间的物体碰撞及物理引擎Accleroation

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

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

  • 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技能,你开发出来的最终手机游戏的效果为如下所示:

image

一、教程开始 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,工程即新建完毕

xcode.png
iosGame.png
03.png

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

04.png

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

删除Action.sks
修改GameScene.swift的代码
设置Scene的尺寸为CGSize(x:1536,y: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


拖动音乐文件

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


Assets.xcassets

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


左侧的导航栏navigator样式

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


Assets图库中的图片尺寸分为1x,2x,3x,只需设置1x图片即可

非常的棒,你已经学会如何导入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设备
Run运行
Simulator模拟器

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

三、SpriteKit Physics 物理引擎

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

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

SpriteKit SKPhysicsBody类物理体的属性图表

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

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

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


新建player玩家飞船节点 属性面板中Sprite Name命名为playerNode
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 拓展函数 返回二个数之间的随机数

import 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外星飞船正向下俯冲

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、监测精灵之间的物体碰撞及物理引擎Accleroation

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

更多游戏教学:iFIERO.COM -- 开源手机游戏教程网,让手机游戏开发变得简单!


iFiero.com
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容