# NinjaPiggyGameTutorial **Repository Path**: iFIERO/NinjaPiggyGameTutorial ## Basic Information - **Project Name**: NinjaPiggyGameTutorial - **Description**: iFIERO -- Mobile Game Tutorial For iOS / iPhone - **Primary Language**: Swift - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2018-06-25 - **Last Updated**: 2022-01-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README /* * * *** 游戏元素使用条款及注意事项 *** * * 游戏中的所有元素全部由iFIERO所原创(除引用之外),包括人物、音乐、场景等, * 创作的初衷就是让更多的游戏爱好者可以在开发游戏中获得自豪感 -- 让手机游戏开发变得简单。 * 秉着开源分享的原则,iFIERO发布的游戏都尽可能的易懂实用,并开放所有源码, * 任何使用者都可以使用游戏中的代码块,也可以进行拷贝、修改、更新、升级,无须再经过iFIERO的同意。 * 但这并不表示可以任意复制、拆分其中的游戏元素: * 用于[商业目的]而不注明出处, * 用于[任何教学]而不注明出处, * 用于[游戏上架]而不注明出处; * 另外,iFIERO有商用授权游戏元素,获得iFIERO官方授权后,即无任何限制! * 请尊重帮助过你的iFIERO的知识产权,非常感谢! * * Created by VANGO杨 && ANDREW陈 * Copyright © 2018 iFiero. All rights reserved. * iFIERO -- 让手机游戏开发变得简单 * www.iFIERO.com * * NinjaPiggy 忍者小猪 在此游戏中您将获得如下技能: * 1、LaunchScreen 学习如何设置游戏启动画面 * 2、Scene 学习如何切换游戏的游戏场景 * 3、Scene Edit 学习直接使用可见即所得操作编辑游戏场景 * 4、Scene Coding 学习纯代码编写一个场景、建立节点、设置音乐 * 5、Random 利用可复用的随机函数生成Enemy * 6、Music 如何添加背景音乐、碰撞时的音效 * 7、Particle 学习如何制造粒子爆炸特效 * 8、Collision 学习有节点与节点之间的碰撞的原理及处理方法 * 9、Animation&Atlas 学习如何导入动画帧及何为Atlas * 10、SKEmitter 学习如何使用SKEmitter产生特效 * */ /* * GameScene.swift * NinjaPiggy * * *** 游戏元素使用条款及注意事项 *** * * 游戏中的所有元素全部由iFIERO所原创(除引用之外),包括人物、音乐、场景等, * 创作的初衷就是让更多的游戏爱好者可以在开发游戏中获得自豪感 -- 让手机游戏开发变得简单。 * 秉着开源分享的原则,iFIERO发布的游戏都尽可能的易懂实用,并开放所有源码, * 任何使用者都可以使用游戏中的代码块,也可以进行拷贝、修改、更新、升级,无须再经过iFIERO的同意。 * 但这并不表示可以任意复制、拆分其中的游戏元素: * 用于[商业目的]而不注明出处, * 用于[任何教学]而不注明出处, * 用于[游戏上架]而不注明出处; * 另外,iFIERO有商用授权游戏元素,获得iFIERO官方授权后,即无任何限制! * 请尊重帮助过你的iFIERO的知识产权,非常感谢! * * Created by VANGO杨 && ANDREW陈 * Copyright © 2018 iFiero. All rights reserved. * iFIERO -- 让手机游戏开发变得简单 * www.iFIERO.com * * NinjaPiggy 忍者小猪 在此游戏中您将获得如下技能: * 1、LaunchScreen 学习如何设置游戏启动画面 * 2、Scene 学习如何切换游戏的游戏场景 * 3、Scene Edit 学习直接使用可见即所得操作编辑游戏场景 * 4、Scene Coding 学习纯代码编写一个场景、建立节点、设置音乐 * 5、Random 利用可复用的随机函数生成Enemy * 6、Music 如何添加背景音乐、碰撞时的音效 * 7、Particle 学习如何制造粒子爆炸特效 * 8、Collision 学习有节点与节点之间的碰撞的原理及处理方法 * 9、Animation&Atlas 学习如何导入动画帧及何为Atlas * 10、SKEmitter 学习如何使用SKEmitter产生特效 * */ import SpriteKit import GameplayKit // ZPosition enum Layer:CGFloat { case ninja case projectile case moster } // 名称 struct Category { static let backgroundName:String = "BG" static let ninjaName :String = "NinjaPiggy" } // 碰撞 struct PhysicsCategory { static let None : UInt32 = 0x1 << 1 static let All : UInt32 = 0x1 << 2 static let Projectile: UInt32 = 0x1 << 3 // 用于碰撞时判断BodyA还是BodyB.CategoryBitMask static let Monster : UInt32 = 0x1 << 4 static let Ninja : UInt32 = 0x1 << 5 // 忽略与飞镖的碰撞 } class GameScene: SKScene,SKPhysicsContactDelegate{ let background = SKSpriteNode(imageNamed: Category.backgroundName) var isFingerOnNinja = false // 手指是否在Ninja里 let maxAspectRatio:CGFloat = CGFloat(16 / 9) let monsterScoreLabelNode:SKLabelNode = SKLabelNode() var monsterScore:Int = 0 // monster score 分数 let ninjaLiveLabelNode :SKLabelNode = SKLabelNode() var ninjaLive:Int = 5 // ninja live 生命 var ninjaNode = SKSpriteNode() // 加入ninja player // Ninja Atlas var ninjaAtlas = SKTextureAtlas() // atlas 文件夹名称 var ninjaTextureArray = [SKTexture]() var ninjaActionRepeat1Times = SKAction() // Ninja手挥动的SKAction touchesBegan调用 // Monsters Atlas var monsterNode = SKSpriteNode() // 加入 monster var monsterAtlas = SKTextureAtlas() var monsterTexturesArray = [SKTexture]() var hitAction = SKAction() var invincible = false // 无敌时刻 override func didMove(to view: SKView) { super.didMove(to: view) // 真实世界的物理重力 physicsWorld.gravity = CGVector(dx: 0.0, dy: -9.8) physicsWorld.contactDelegate = self // 加入音乐 hitAction = SKAction.playSoundFileNamed("hitNinja", waitForCompletion: false) addBg() addLogo() addNinja() addScore() /* 1.SK.Action无限产生 monsters let actionAddMonster = SKAction.run { self.addMonsters() } run(SKAction.repeatForever(SKAction.sequence([ actionAddMonster,SKAction.wait(forDuration: TimeInterval(1))]))) */ //2.用Timer每隔1s调用 addMonster Timer.scheduledTimer(timeInterval: TimeInterval(1.0), target: self, selector: #selector(addMonsters), userInfo: nil, repeats: true) addFloor() } //MARK:- 加入背景 option+Command+<-(箭头) 折叠 func addBg(){ background.anchorPoint = CGPoint(x: 0.5, y: 0.5) // default background.position = CGPoint(x: size.width/2, y: size.height/2) // background.zRotation = CGFloat.pi / 8 background.name = "background" background.zPosition = -1 addChild(background) // 背景音乐 let backgroundMusic = SKAudioNode(fileNamed: "background-music-aac.caf") backgroundMusic.autoplayLooped = true addChild(backgroundMusic) } //MARK:- 加入logo func addLogo(){ let logo = SKSpriteNode(imageNamed: "logo") logo.position = CGPoint(x: self.size.width / 2, y: self.frame.height - (self.frame.height / 5)) logo.zPosition = Layer.ninja.rawValue logo.setScale(0.7) logo.name = "logo" self.addChild(logo) } // MARK: - 分数 func addScore(){ // NINJA ninjaLiveLabelNode.text = "NINJA:\(ninjaLive)" ninjaLiveLabelNode.color = SKColor.white ninjaLiveLabelNode.fontSize = 60 ninjaLiveLabelNode.setScale(1.0) ninjaLiveLabelNode.zPosition = 1 ninjaLiveLabelNode.position = CGPoint(x: 150, y: self.frame.height - (self.frame.height / 5)) ninjaLiveLabelNode.name = "ninjaLiveLabel" self.addChild(ninjaLiveLabelNode) // monster monsterScoreLabelNode.text = "MONSTER:\(monsterScore)" monsterScoreLabelNode.color = SKColor.white monsterScoreLabelNode.fontSize = 60 monsterScoreLabelNode.setScale(1.0) monsterScoreLabelNode.zPosition = 1 monsterScoreLabelNode.position = CGPoint(x: 150 + 300, y: self.frame.height - (self.frame.height / 5)) monsterScoreLabelNode.name = "monsterLabel" self.addChild(monsterScoreLabelNode) } //MARK:- 加入Ninja Player func addNinja(){ ninjaAtlas = SKTextureAtlas(named: "NinjaPiggy") for i in 1...ninjaAtlas.textureNames.count { let imageName = "NinjaPiggy0\(i).png" ninjaTextureArray.append(SKTexture(imageNamed: imageName)) } ninjaNode = SKSpriteNode(imageNamed: ninjaAtlas.textureNames[2]) // 加入物理实体 ninjaNode.physicsBody = SKPhysicsBody(rectangleOf: ninjaNode.size) ninjaNode.physicsBody?.affectedByGravity = true // FIXME: 修复Ninja发射飞镖时的反作用力,否则Ninja发射飞镖时会反弹; // physicsBody=>CategoryBitMask,ContactTestBitMask,collisionBitMask ninjaNode.anchorPoint = CGPoint (x: 0.5, y: 0.5) ninjaNode.position = CGPoint(x: self.frame.size.width / 8 + 150 , y: (self.frame.size.height / 4 - 100)) ninjaNode.setScale(1.0) ninjaNode.name = "ninja" self.addChild(ninjaNode) // 手的动画 Animation let ninjaActionAnimation = SKAction.animate(with: ninjaTextureArray, timePerFrame: 0.1) ninjaActionRepeat1Times = SKAction.repeat(ninjaActionAnimation, count: 1) // 1.属于哪个对像 ninjaNode.physicsBody?.categoryBitMask = PhysicsCategory.Ninja // 2.和谁发生碰撞 ninjaNode.physicsBody?.contactTestBitMask = PhysicsCategory.Monster // 3.碰撞后会弹开吗? bounce off ninjaNode.physicsBody?.collisionBitMask = PhysicsCategory.None ninjaNode.physicsBody?.usesPreciseCollisionDetection = false } //MARK:- 随机产生 单个 Monster Y 轴 + 从右往左移动+移出屏幕 removeFromParent @objc func addMonsters(){ monsterAtlas = SKTextureAtlas(named: "Bomb") for i in 1...monsterAtlas.textureNames.count { let imageName = "Bomb0\(i).png" monsterTexturesArray.append(SKTexture(imageNamed: imageName)) } monsterNode = SKSpriteNode(imageNamed: monsterAtlas.textureNames[0]) monsterNode.anchorPoint = CGPoint(x: 0.5, y: 0.5) monsterNode.zPosition = Layer.projectile.rawValue monsterNode.name = "monster" // Y值随机位置 let randomPositionY = CGFloat.random(min: monsterNode.size.height , max: size.height - monsterNode.size.height) monsterNode.position = CGPoint(x: size.width + monsterNode.size.width, y: randomPositionY) // 加monster monsterNode.setScale(0.9) self.addChild(monsterNode) // 物理 monsterNode.physicsBody = SKPhysicsBody(circleOfRadius: self.monsterNode.size.width / 2) monsterNode.physicsBody?.affectedByGravity = false // 不会重力影响 // monsterNode.physicsBody?.isDynamic = false monsterNode.physicsBody?.categoryBitMask = PhysicsCategory.Monster monsterNode.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile | PhysicsCategory.Ninja monsterNode.physicsBody?.collisionBitMask = PhysicsCategory.None monsterNode.physicsBody?.usesPreciseCollisionDetection = false /// MONSTER自身动画 let monsterAction = SKAction.animate(with: monsterTexturesArray, timePerFrame: 0.1) let monsterActionRepeatForever = SKAction.repeatForever(monsterAction) monsterNode.run(monsterActionRepeatForever, withKey: "Monster") /// 从右往左的随机速度 let duration = CGFloat.random(min: CGFloat(1.0), max: CGFloat(3.8)) let actionMoveToLeft = SKAction.move(to: CGPoint(x: CGFloat(-monsterNode.size.width), y: randomPositionY), duration: TimeInterval(duration)) let actionRemove = SKAction.removeFromParent() monsterNode.run(SKAction.sequence([actionMoveToLeft,actionRemove])) } //MARK:- 加入物理地板 edge-base 让 Ninja站在物理世界的地板上 func addFloor(){ let yPos = self.frame.size.height / 4 - 100 - 1 // (Ninja脚的高度 - 1) let startPoint = CGPoint(x: 0, y: yPos) let endPoint = CGPoint(x:size.width,y:yPos) let floorNode = SKNode() floorNode.physicsBody = SKPhysicsBody(edgeFrom: startPoint, to: endPoint) floorNode.name = "floor" addChild(floorNode) } override func touchesBegan(_ touches: Set, with event: UIEvent?) { if let touch = touches.first { let touchLocation = touch.location(in: self) if ninjaNode.contains(touchLocation){ isFingerOnNinja = true } //ninja发射飞镖 if !isFingerOnNinja { let actionFire = SKAction.playSoundFileNamed("pew-pew-lei.caf", waitForCompletion: false) run(actionFire) // 扔Projectile ninjaNode.run(ninjaActionRepeat1Times, withKey: "FireProjectile") } } } override func touchesMoved(_ touches: Set, with event: UIEvent?) { if isFingerOnNinja { let touch = touches.first let touchLocation = touch!.location(in: self) let previousLocation = touch!.previousLocation(in: self) let ninjaNode = self.ninjaNode //let ninjaY = (ninjaNode.position.y) + (touchLocation.y - previousLocation.y) var ninja = (ninjaNode.position) + (touchLocation - previousLocation) // ninjaNode.position = CGPoint(x: (ninjaNode.position.x), y: ninja.y) // FIXME:限定移动的区域 min ,max // ninja.x = min(ninja.x,ninjaNode.size.width / 2) if ninja.x < self.ninjaNode.size.width / 2 { ninja.x = self.ninjaNode.size.width / 2 } // ninja.x = max(ninja.x,(self.size.width - self.ninjaNode.size.width)) if ninja.x > self.size.width - self.ninjaNode.size.width { ninja.x = self.size.width - self.ninjaNode.size.width } if ninja.y < (self.frame.size.height / 4 - 100) { ninja.y = self.frame.size.height / 4 - 100 } ninjaNode.position = CGPoint(x: ninja.x, y: ninja.y) } } //MARK: -- 生成飞镖并发射 override func touchesEnded(_ touches: Set, with event: UIEvent?) { isFingerOnNinja = false guard let touch = touches.first else { return } let touchLocation = touch.location(in: self) // 生成飞镖 let projectileNode = SKSpriteNode(imageNamed: "projectile") // 飞镖的位置位于Ninja的手上 0.5,0.5 X轴右移 ninjaNode.size.with 1/2 projectileNode.position = CGPoint(x: ninjaNode.position.x + self.ninjaNode.size.width / 2 , y: ninjaNode.position.y) projectileNode.physicsBody = SKPhysicsBody(circleOfRadius: projectileNode.size.width / 2) projectileNode.physicsBody?.isDynamic = false // 不受重力影响 projectileNode.name = "projectile" projectileNode.setScale(CGFloat(maxAspectRatio)) projectileNode.zPosition = Layer.projectile.rawValue projectileNode.setScale(1.0) let offset = touchLocation - projectileNode.position // print("projectile offset:\(offset)") if (offset.x < 0 ) {return } // Ninja Never Look Back self.addChild(projectileNode) // 加入碰撞对象 projectileNode.physicsBody?.categoryBitMask = PhysicsCategory.Projectile projectileNode.physicsBody?.contactTestBitMask = PhysicsCategory.Monster // projectileNode.physicsBody?.collisionBitMask = PhysicsCategory.None // 精细的对象detect,才可以飞行中的monster进行碰撞 projectileNode.physicsBody?.usesPreciseCollisionDetection = true /* SKEmitter:加入酷酷飞镖粒子拖影 图层关系: projectNode -> emitterNode -> trailNode */ // 1.新建一个节点 (发生碰撞时也须移除此节点) let trailNode = SKNode() trailNode.zPosition = Layer.projectile.rawValue trailNode.name = "trail" addChild(trailNode) // 2.新建粒子效果 let emitterNode = SKEmitterNode(fileNamed: "ProjectileTrail")! emitterNode.name = "emitter" // 3.粒子效果的子节点为 trailNode emitterNode.targetNode = trailNode // 4.飞镖Node加上子节点 emitter projectileNode.addChild(emitterNode) /* SKEmitter 此方法只有一个粒子,无拖影效果 图层关系:projectNode -> emitterNode let emitterNode = SKEmitterNode(fileNamed: "ProjectileTrail")! emitterNode.zPosition = Layer.projectile.rawValue - 1 // 位于projectile下层 projectileNode.addChild(emitterNode) */ let actionRotate = SKAction.rotate(byAngle: CGFloat(-Double.pi/2), duration: 0.2) let actionRotateForever = SKAction.repeatForever(actionRotate) projectileNode.run(actionRotateForever) let direction = offset.normalized() // 发射方向/每单位 let shootAmount = direction * self.frame.size.width // 足够长 射出屏幕 let realDestination = shootAmount + projectileNode.position let actionMove = SKAction.move(to: realDestination, duration: 3.0) let actionRemove = SKAction.removeFromParent() projectileNode.run(SKAction.sequence([ actionMove, actionRemove, SKAction.run { trailNode.removeFromParent() // MARK:除了projectile要移除,新建的trailNode粒子节点也需移除 } ])) } // 发生碰撞时的代理 func didBegin(_ contact: SKPhysicsContact) { //顺序 project < monster < ninja var bodyA:SKPhysicsBody var bodyB:SKPhysicsBody if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask { bodyA = contact.bodyA bodyB = contact.bodyB }else { bodyA = contact.bodyB bodyB = contact.bodyA } // 使用这种switch判断方法,比判断哪个碰撞的对像是bodyA,bodyB更快捷,个人比较喜欢用,awesome! // print("project:",PhysicsCategory.Projectile,"-monster:",PhysicsCategory.Monster,"-ninja:",PhysicsCategory.Ninja) let contactBitMASK = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask switch contactBitMASK { // 击中monster case PhysicsCategory.Projectile | PhysicsCategory.Monster: projectileCollideWithMonster(projectileNode: bodyA.node as! SKSpriteNode, monsterNode: bodyB.node as! SKSpriteNode) case PhysicsCategory.Ninja | PhysicsCategory.Monster: // 得判断ninja是bodyA或者bodyB if bodyA.categoryBitMask == PhysicsCategory.Ninja{ // print("bodyA is not ninja piggy") // ninjaCollideWithMonster(ninjaNode: bodyA.node as! SKSpriteNode, monsterNode: bodyB.node as! SKSpriteNode) }else { /* * 为防止手指一直拖动Ninja和Monster发生碰撞,在碰撞发生的第一时刻didBegin * 就立刻把ninjaNode的物理实体设为不会再发生碰撞 => ninjaNode.physicsBody?.categoryBitMask = 0 * 待ninjaCollideWithMonster函数的Action执行完毕后 * 再设置 ninjaNode 可以发生碰撞 => ninjaNode.physicsBody?.categoryBitMask = PhysicsCategory.Ninja */ ninjaNode.physicsBody?.categoryBitMask = 0 ninjaCollideWithMonster(ninjaNode: bodyB.node as! SKSpriteNode, monsterNode: bodyA.node as! SKSpriteNode) } default: break } } //MARK: - 飞镖击中Monsters func projectileCollideWithMonster(projectileNode:SKSpriteNode,monsterNode:SKSpriteNode){ // monster score 加分 self.monsterScore += 1 monsterScoreLabelNode.text = "MONSTER:\(monsterScore)" // 切换场景 + 播放胜利的音乐 if monsterScore >= 30 { // 播放胜利的音乐 let wonAction = SKAction.playSoundFileNamed("won", waitForCompletion: false) run(wonAction, completion: { // you win 切换场景 Scene let reveal = SKTransition.doorsCloseHorizontal(withDuration: 0.5) //let gameWonScene = SKScene(fileNamed: "GameWon") let gameWonScene = GameWon(fileNamed: "GameWon") gameWonScene?.size = self.size gameWonScene?.scaleMode = .aspectFill self.view?.presentScene(gameWonScene!, transition: reveal) }) } // 击中粒子效果 Particle let explosion = SKEmitterNode(fileNamed: "Explosion")! explosion.position = monsterNode.position self.addChild(explosion) // 击中的音乐 let colisionAction = SKAction.playSoundFileNamed("yew-yew.wav", waitForCompletion: false) let removeMonsterAction = SKAction.run { monsterNode.removeFromParent()} run(SKAction.sequence([ SKAction.run { //MARK:- 移除所有节点projectile的child的emitter效果 设置projectile不可见,让飞出屏幕后自动消除 projectileNode.removeAllChildren() projectileNode.isHidden = true projectileNode.physicsBody?.categoryBitMask = 0 // 不会再发生下一个碰撞 }, removeMonsterAction, colisionAction, SKAction.wait(forDuration: 0.2), SKAction.run { explosion.removeFromParent() //移除爆炸效果 } ])) } //MARK: - Ninja被Monster击中 func ninjaCollideWithMonster(ninjaNode:SKSpriteNode,monsterNode:SKSpriteNode){ //MARK: 进阶 monster 击中 ninja时的 ACTION Sequence // 1.变成绿色 let turnGreeenAction = SKAction.colorize(with: SKColor.green, colorBlendFactor: 1.0, duration: 0.2) // texture恢复ninja的色彩 colorBlendFactor 默认是0.0 也就是纹理不做改变直接呈现 let removeGreenAction = SKAction.run { ninjaNode.colorBlendFactor = 0.0 // ninjaNode.texture = SKTexture(imageNamed: self.ninjaAtlas.textureNames[2]) } // 2.闪烁 let fadeIn = SKAction.fadeAlpha(to: 0, duration: 0.2) let fadeOut = SKAction.fadeAlpha(to: 1, duration: 0.2) let blinkAction = SKAction.repeat(SKAction.sequence([fadeIn,fadeOut]), count: 5) // 4.执行Action Sequence ninjaNode.run(SKAction.sequence([ self.hitAction, turnGreeenAction, blinkAction, removeGreenAction, SKAction.run({ [weak self] in // 等待ACTION结束后 开始计分; // monster score 加分 self?.ninjaLive -= 1 if ((self?.ninjaLive)! < 0) { self?.ninjaLive = 0 } // 切换场景 if (self?.ninjaLive)! <= 0 { // 播放失败的音乐 let loseAction = SKAction.playSoundFileNamed("lose", waitForCompletion: false) self?.run(loseAction, completion: { //monsterNode.run(SKAction.removeFromParent()) //ninjaNode.run(SKAction.removeFromParent()) // you win 切换场景 Scene let reveal = SKTransition.doorsCloseHorizontal(withDuration: 0.5) let gameLoseScene = SKScene(fileNamed: "GameLose") gameLoseScene?.size = (self?.size)! gameLoseScene?.scaleMode = .aspectFill self?.view?.presentScene(gameLoseScene!, transition: reveal) }) } self?.ninjaLiveLabelNode.text = "NINJA:\(self?.ninjaLive ?? 5)" }), SKAction.run({ // 设次设置可以和Ninja发生碰撞 self.ninjaNode.physicsBody?.categoryBitMask = PhysicsCategory.Ninja }) ])) } override func update(_ currentTime: TimeInterval) { } } MORE GAME TUTORIAL : HTTP://www.iFIERO.COM