美文网首页
iOS版本使用SpriteKit框架实现坦克大战游戏

iOS版本使用SpriteKit框架实现坦克大战游戏

作者: 帅聪哥 | 来源:发表于2019-03-08 17:36 被阅读0次

    开场白:

    话说这段时间对于用SpriteKit游戏突然产生浓厚兴趣,这不利用空闲时间完成了一个简易版的坦克大战。

    额,因为gif图转换太慢了,后期在补上,先上传几张图片看看效果哈! 开始前 开战中
    开战中 选择难易程度
    看到效果图,如果感觉还行,有必要继续看的童鞋继续哈,下面针对比较重要且有难点的地方讲解一下了。

    重点一:游戏方向控制盘,具体实现可看下游戏方向盘实现

    个人比较喜欢王者荣耀中的操作盘,就按照它的样子模仿吧,但是由于缺少素材,最终出来的效果起码还算及格哈。

    这个是游戏手柄部分代码

    /**
     游戏控制手柄
     */
    class MyTankControlHandler: UIView {
    
        ///内容显示
        var contentView:UIView?
        ///内部小圈圈
        var circelView:MyTankMidCircleView?
        ///最外层的方向箭头
        var directionImageView:UIImageView?
        ///是否开始触发
        var isBeginMove:Bool = false
        ///是否在中间
        var isStandInMiddle:Bool = false
        ///移动的比例,0-1,越高则移动的越快
        var moveRatio:CGFloat = 0
        ///方向值
        var direction:CGFloat = 0
        ///定时器,用来监听执行isBeginMove
        var displayLinkTimer:CADisplayLink? = nil
        
        
        weak var delegate : MyTankControlHandlerDelegate?
        
        class func gameHandler ()->MyTankControlHandler {
            return MyTankControlHandler.init(frame: CGRect(x: 0, y: 0, width: 170, height: 170))
        }
    

    来分析一下思路:
    1、我们的操作盘用来做什么,最终目的只有一个:将当前方向值告诉外界,外界只要成为代理即可获取方向值。
    2、不管是点按还是长按,都会触发,而且还得考虑到手势的位置,假设操作手柄圆圈移动的最大距离为100,那么移动的速度是否根距离有关呢,再者,当手势点在中心多少范围内算不算不移动呢,当移动点距离中心大于等于100的时候,速度达到最快。
    3、方向值的确认:这个得好好温习下数学的三角函数的知识了,
    //atan 和 atan2 都是求反正切函数,如:有两个点 point(x1,y1), 和 point(x2,y2);
    //那么这两个点形成的斜率的角度计算方法分别是:
    //float angle = atan( (y2-y1)/(x2-x1) );
    //或
    //float angle = atan2( y2-y1, x2-x1 );
    let angle = atan2(position.y-midY, position.x-midX)
    在拖动的时候和touchBegin的时候更新方向值
    4、如果比较省事又方便的将方向值进行回传呢: 这里我一开始也走了挺多弯路,因为在拖动手势和touchBegin在统计长按上代码上比较多且又麻烦。最终使用CADisplayLink定时器,在定时器方法中,只要触发了方向盘就一直回调即可,

    @objc func moveUpdateAction () {
            ///触发了方向盘
            if (self.isBeginMove == false){
                return
            }
            ///是否当前在中间范围内,则考虑用户当前是否不动
            if self.isStandInMiddle == true {
                return
            }
            if self.delegate != nil {
                self.delegate?.controlHandlerMove(handler: self, directionValue: self.direction,moveRatio:self.moveRatio)
            }
        }
    

    5、中间的圆圈怎样才能保证随着手势动起来:
    在拖动的时候,根据得到的position,然后判断是否超出边界给予新的position,赋值给圆圈即可:

    //当前拖动的位置,在拖动手势回调中
     let position =  gester.location(in: self.contentView)
    //根据中心点、圆的大小、当前拖动位置得到新的位置赋值给圆圈
    let resultPoint = self.isCircleIn(center: CGPoint(x: ContentWidth*0.5, y: ContentHeight*0.5), rect: (self.contentView?.bounds)!, point: position)
    

    具体的判断方法:得到新的position并赋值给圆圈即可

    ///判断当前的位置是否超出一定范围内
        func isCircleIn(center:CGPoint,rect:CGRect,point:CGPoint) ->CGPoint {
            //就是要算出点到圆心的距离是否大于半径
            var distance:Float = 0;
            var resultPoint:CGPoint = point
            var moveRatio :Float = 0
            
            //大圆半径
            let radius = Float(rect.size.width*0.5)
            //小圆半径
            let circleRadicu = Float((self.circelView?.bounds.size.width)!*0.5)
            //半径减掉内圆的半径才是最长的移动距离
            let calculateRadius = radius - circleRadicu
            
            if(point.x == center.x && point.y == center.y){
                distance = 0
            }else if(point.y == center.y){
                distance = fabsf(Float(center.x - point.x))
                if(distance >= calculateRadius) {
                    if(center.x > point.x){
                        resultPoint = CGPoint(x: CGFloat(circleRadicu), y: resultPoint.y)
                    }else{
                        resultPoint = CGPoint(x: rect.size.width-CGFloat(circleRadicu), y: resultPoint.y)
                    }
                    moveRatio = 1
                }else{
                    moveRatio = distance / calculateRadius
                }
            }else if(point.x == center.x){
                distance = fabsf(Float(center.y - point.y))
                if(distance >= calculateRadius){
                    if(center.y > point.y){
                        resultPoint = CGPoint(x: resultPoint.x, y: CGFloat(circleRadicu))
                    }else{
                        resultPoint = CGPoint(x: resultPoint.x, y: rect.size.height-CGFloat(circleRadicu))
                    }
                    moveRatio = 1
                }else{
                    moveRatio = distance / calculateRadius
                }
            }else{
                let xValue = fabsf(Float(center.x - point.x))
                let yValue = fabsf(Float(center.y - point.y))
                distance = hypotf(xValue, yValue)
                if(distance >= calculateRadius){
                    moveRatio = 1
                    let angle = atan2(point.y-center.y, point.x-center.x)
                    let sinYValue = sin(angle)*CGFloat(calculateRadius)//正弦,就是y值方向
                    let cosXValue = cos(angle)*CGFloat(calculateRadius)//余弦,就是x值方式
                    //print("angle:\(angle) , 垂直:\(yValue),水平:\(xValue)")
                    //当在右上角方向时,xValue为正 yValue为负
                    if(cosXValue > 0 && sinYValue < 0){
                        let fabsY = CGFloat(fabsf(Float(sinYValue)))
                        resultPoint = CGPoint(x: cosXValue + CGFloat(radius), y: CGFloat(radius) - fabsY)
                    }else if(xValue > 0 && yValue > 0){//当在右下角方向时,xValue 和 yValue 都为正
                        resultPoint = CGPoint(x: cosXValue + CGFloat(radius), y: sinYValue + CGFloat(radius))
                    }else if(cosXValue < 0 && sinYValue > 0){//当在左下角方向时,xValue为负 yValue为正
                        let fabsX = CGFloat(fabsf(Float(cosXValue)))
                        resultPoint = CGPoint(x: CGFloat(radius) - fabsX, y: sinYValue + CGFloat(radius))
                    }else if(cosXValue < 0 && sinYValue < 0){//当在左上角方向时,xValue 和 yValue 都为负
                        let fabsY = CGFloat(fabsf(Float(sinYValue)))
                        let fabsX = CGFloat(fabsf(Float(cosXValue)))
                        resultPoint = CGPoint(x: CGFloat(radius) - fabsX, y: CGFloat(radius) - fabsY)
                    }
                }else{
                    moveRatio = distance / calculateRadius
                }
            }
            self.moveRatio = CGFloat(moveRatio)
            return resultPoint
        }
    
    

    重点二:游戏中的坐标系和我们正常app中的坐标系不一样

    坐标系.JPG

    而且角度也是反的,就比如正常下90度的方向应该是垂直往下,但是在游戏中是垂直往上的,切记哈。

    重点三:坦克精灵 class TankSpriteNode: SKSpriteNode

    1、坦克根据当前的方向发射炮弹
    首先如果创造炮弹:先看一下代码

    func createBullte (isEnmy:Bool) ->SKSpriteNode {
            let w:CGFloat = 5
            let bullte = SKSpriteNode.init(texture: nil, size: CGSize(width: w, height: w))
            bullte.name = "bullte"
            bullte.physicsBody = SKPhysicsBody.init(circleOfRadius: w*0.5, center: CGPoint(x: 0.5, y: 0.5))
            bullte.physicsBody?.categoryBitMask = isEnmy ? bullteEnmyCategory : bullteCategory
            if(isEnmy == true){
                bullte.physicsBody?.contactTestBitMask = tankCategory | frontierCatetory | bullteCategory
                bullte.physicsBody?.collisionBitMask = tankCategory//可以和哪些体发送碰撞
            }else{
                bullte.physicsBody?.contactTestBitMask = tankEnmyCategory | frontierCatetory | bullteEnmyCategory
            }
            bullte.physicsBody?.allowsRotation = false
            bullte.physicsBody?.isDynamic = true
            
            let shape = SKShapeNode.init(rectOf: CGSize.init(width: w, height: w), cornerRadius: w * 0.5)
            shape.position = CGPoint(x: 0.5, y: 0.5)
            shape.fillColor = isEnmy ? SKColor.black:SKColor.red
            bullte.addChild(shape)
            
            return bullte
        }
    

    各种物理体的标识

    ///本身子弹
    let bullteCategory:UInt32 = 0x1 << 4
    ///敌方子弹
    let bullteEnmyCategory:UInt32 = 0x1 << 5
    ///本身坦克
    let tankCategory:UInt32 = 0x1 << 6
    ///敌方坦克
    let tankEnmyCategory:UInt32 = 0x1 << 7
    ///边界
    let frontierCatetory:UInt32 = 0x1 << 8
    
    针对几个比较重要的参数讲解一下

    categoryBitMask就是可以理解给当前的物理体做标记,假设是bullteCategory
    contactTestBitMask在物理场景中可以与哪些其他物体发生联系,假设设置为tankEnmyCategory,那么在场景碰撞后,才会有回调。
    collisionBitMask表示可以与哪些物理体发生碰撞,所以敌方坦克就不能与敌方坦克发生碰撞了,直接可以穿透了哈。
    isDynamic,字面意思表示是否是动态的,这个字段我参透的不够彻底,只是知道设置后有什么效果,这里就不解释了,看官方解释哈:The default value is true. If the value is false, the physics body ignores all forces and impulses applied to it. This property is ignored on edge-based bodies; they are automatically static.

    第二步:炮弹发射的操作
    ///下面注释都有哈

    ///发射子弹
        func sendBullte(isEnmy:Bool) {
            let bullte = self.createBullte(isEnmy: isEnmy)
            if self.scene == nil {
                return
            }
            ///子弹需要从坦克的头部开出,所以子弹相对应坦克的位置也是会变的
            var bullteXDistance:CGFloat = 0
            var bullteYDistance:CGFloat = 0
            
            self.scene?.addChild(bullte)
            ///就是默认往上的推力为0.25,可以自己设置,越大速度越快
            let defaultValue:CGFloat = 0.25
            
            var xValue:CGFloat = 0
            var YValue:CGFloat = 0
            if (self.direction == 0){
                xValue = defaultValue
                bullteXDistance = 20
            }else if(self.direction == CGFloat(Double.pi/2)){
                YValue = -defaultValue
                bullteYDistance = -20
            }else if(self.direction == CGFloat(Double.pi) || self.direction == -CGFloat(Double.pi)){
                xValue = -defaultValue
                bullteXDistance = -20
            }else if(self.direction == -CGFloat(Double.pi/2)){
                YValue = defaultValue
                bullteYDistance = 20
            }else{
                //假设斜边为1
                YValue = -sin(self.direction)*defaultValue//正弦,就是y值方向
                xValue = cos(self.direction)*defaultValue//余弦,就是x值方式
                bullteYDistance = -sin(self.direction)*20
                bullteXDistance = cos(self.direction)*20
            }
            ///相对于坦克的位置
            bullte.position = CGPoint(x:self.position.x+bullteXDistance,y:self.position.y+bullteYDistance)
            bullte.physicsBody?.applyImpulse(CGVector(dx: xValue, dy: YValue))
        }
    

    2、敌方坦克的走位逻辑计算
    class TankEnemyTank:TankSpriteNode继承TankSpriteNode
    首先思路是这样的:敌方坦克只能垂直或水平移动,而且速度比我方坦克要慢2.5倍,然后根据难易程度设置几个固定的位置,在将当前我发坦克位置进行计算得到x轴与y轴之间的距离的合,就是坦克移动的总距离了,然后按照每一帧(1/60*2.5)的距离进行计算时间,等执行完之后就通过定时器进行回调,在定时器到达之后如果坦克没有被消灭,则继续轮回重新操作一遍,以下是代码实现部分:

    ///移动到目的地,就是冲着对方坦克位置去攻击
        func moveToDesination(position:CGPoint)->Void {
            
            ///首先计算自己的位置与目标位置的最长距离,因为只能自动坦克只能沿着x轴或y轴直线行驶
            let selfPosition = self.position
            let xMargin = fabsf(Float(selfPosition.x - position.x))
            let yMargin = fabsf(Float(selfPosition.y - position.y))
            let totalMargin = xMargin + yMargin
            let totalDuration = TimeInterval(2.5/60.0*totalMargin)
            
            var resultDuration:TimeInterval = totalDuration
           ///组装动画集合,方便一起执行
            var actions:Array<SKAction> = []
    
    ///先移动到和目标x方向一致,为了让它们不至于每次都沿着一个方向,给个随机
                let suijiValue = arc4random_uniform(2)
                if suijiValue == 0 {
                    if(selfPosition.x > position.x){
                        if(self.direction != CGFloat(Double.pi)){
                            self.direction = CGFloat(Double.pi)
                        }
                    }else{
                        if(self.direction != 0){
                            self.direction = 0
                        }
                    }
                    let firstDirection = self.direction
                    ///坦克第一次转向
                    let firstAction = SKAction.rotate(toAngle: -firstDirection, duration: 0.25)
                    resultDuration += 0.25
                    actions.append(firstAction)
                    ///移动到第一个点
                    let firstPosition = CGPoint(x: position.x, y: selfPosition.y)
                    let firstDuration = totalDuration * TimeInterval(xMargin/totalMargin)
                    let secondAction = SKAction.move(to: firstPosition, duration: firstDuration)
                    actions.append(secondAction)
                    
                    var secondDirection:CGFloat = 0
                    if(selfPosition.y > position.y){
                        secondDirection = CGFloat(Double.pi/2)
                    }else{
                        secondDirection = -CGFloat(Double.pi/2)
                    }
                    ///坦克第二次转向
                    let thirdAction = SKAction.rotate(toAngle: -secondDirection, duration: 0.25)
                    resultDuration += 0.25
                    actions.append(thirdAction)
                    
                    let changDirectionAction = SKAction.run {
                        self.direction = -secondDirection
                    }
                    actions.append(thirdAction)
                    actions.append(changDirectionAction)
                    ///坦克第二次移动
                    let fourAction = SKAction.move(to: position, duration: totalDuration-firstDuration)
                    actions.append(fourAction)
    }
    let resultAction = SKAction.sequence(actions)
            self.run(resultAction)
            if(self.timer != nil){
                self.timer?.invalidate()
                self.timer = nil
            }
            
            let timer = Timer.scheduledTimer(withTimeInterval: resultDuration, repeats: false) { (timer:Timer) in
                self.moveArriveAction()
            }
            self.timer = timer
    
    针对游戏难易程度专门设置了一个管理者,并且管理着敌方坦克的重建和初始位置,懒得介绍哈,直接上代码:
    /**游戏难易程度管理者*/
    class MyTankHardSolver: NSObject ,TankEnemyTankDelegate{
    
        class share {
            static let manager = MyTankHardSolver()
        }
        
        ///难度等级,默认为1级
        var hardGrade:Int = 1
        //坦克基数,每次创建几个
        var baseCount:Int = 2
        ///设置了一个最大分值,根据难易程度来定
        var maxScore:Int = 0
        ///专门存放坦克
        var automaticTankArray:Array<TankEnemyTank> = []
        ///持有场景对象
        weak var gameScene:MyTankeScene!
        ///总分
        var score:Int = 0
        
        func gameStart() {
            self.score = 0
            self.automaticTankArray.removeAll()
            startGameWithDestinationPostion(position: (self.gameScene.myTank?.position)!)
        }
        
        func gameOverAction () {
            for tank in self.automaticTankArray {
                tank.running = false
                tank.removeAllActions()
            }
        }
        
        ///初始化操作
        func startGameWithDestinationPostion(position:CGPoint) {
            
            let positionList = getTankPositonDataArrayWithGrade()
            for value:NSValue in positionList {
                
                let texture = SKTexture(imageNamed: "mytank_tank")
                let enmy = TankEnemyTank.init(texture: texture, color: UIColor.clear, size: CGSize(width: tankWidth, height: tankHeight))
                let showPosition = value.cgPointValue
                enmy.position = showPosition
                enmy.beginPosition = showPosition
                self.gameScene.addChild(enmy)
                enmy.moveToDesination(position: position)
                automaticTankArray.append(enmy)
                enmy.delegate = self
            }
        }
        /**根据难易程度随便定了几个位置*/
        func getTankPositonDataArrayWithGrade () ->Array<NSValue> {
            
            var list:Array<NSValue> = []
            let width = self.gameScene.size.width
            let height = self.gameScene.size.height
            if(width == 0 || height == 0){
                return list
            }
            
            let firstPoint = CGPoint(x: 50, y: height-tankHeight-44)
            let point1Value = NSValue.init(cgPoint: firstPoint)
            
            let secondPoint = CGPoint(x: width-tankWidth, y: 50)
            let point2Value = NSValue.init(cgPoint: secondPoint)
            list.append(point1Value)
            list.append(point2Value)
            self.maxScore = 20
            if self.hardGrade == 2 {
                let thirdPoint = CGPoint(x: 50, y: 50)
                let point3Value = NSValue.init(cgPoint: thirdPoint)
                
                let fourPoint = CGPoint(x: width-tankWidth, y: height-tankHeight-44)
                let point4Value = NSValue.init(cgPoint: fourPoint)
                
                list.append(point3Value)
                list.append(point4Value)
                self.maxScore = 40
                self.baseCount = 4
            }else if (self.hardGrade >= 3){
                let thirdPoint = CGPoint(x: 50, y: 50)
                let point3Value = NSValue.init(cgPoint: thirdPoint)
                
                let fourPoint = CGPoint(x: width-tankWidth, y: height-tankHeight-44)
                let point4Value = NSValue.init(cgPoint: fourPoint)
                
                list.append(point3Value)
                list.append(point4Value)
                
                let fivePoint = CGPoint(x: (width-tankWidth)*0.5, y: 50)
                let sixPoint = CGPoint(x: (width-tankWidth)*0.5, y: height - 50-44)
                
                let point5Value = NSValue.init(cgPoint: fivePoint)
                let point6Value = NSValue.init(cgPoint: sixPoint)
                
                list.append(point5Value)
                list.append(point6Value)
                self.maxScore = 60
                self.baseCount = 6
            }
        
            return list
        }
        
        
        //MARK:TankEnemyTankDelegate
        func tankEnemyArrive(tank: TankEnemyTank) {
            tank.moveToDesination(position: (self.gameScene.myTank?.position)!)
        }
        
        //被击中
        func tankEnemyKilled(tank: TankEnemyTank) {
            self.score += 1
            let brokenCount = self.maxScore - self.baseCount
            ///游戏通关
            if(self.score >= self.maxScore){
                
                return
            }
            ///则不需要再创造坦克了
            if(self.score > brokenCount){
                return
            }
            for (index,value) in self.automaticTankArray.enumerated() {
                if value == tank {
                    self.automaticTankArray.remove(at: index)
                    //重新再弄一个出来
                    let texture = SKTexture(imageNamed: "mytank_tank")
                    let enmy = TankEnemyTank.init(texture: texture, color: UIColor.clear, size: CGSize(width: tankWidth, height: tankHeight))
                    let orginPostion = tank.beginPosition!
                    var showPosition = tank.beginPosition!
                    ///这样的目的是不至于每次都在同一个位置出现
                    var suijiXValue = CGFloat(arc4random_uniform(40))
                    var suijiYValue = CGFloat(arc4random_uniform(40))
                    let boolValue = arc4random_uniform(2)
                    if(boolValue == 0){
                        suijiXValue = -CGFloat(suijiXValue)
                        suijiYValue = -CGFloat(suijiYValue)
                    }
                    showPosition = CGPoint(x: showPosition.x+suijiXValue, y: showPosition.y+suijiYValue)
                    enmy.position = showPosition
                    enmy.beginPosition = orginPostion
                    enmy.isHidden = true
                    self.gameScene.addChild(enmy)
                    let action = SKAction.wait(forDuration: 2.0)
                    enmy.run(action)
                    enmy.isHidden = false
                    enmy.moveToDesination(position: (self.gameScene.myTank?.position)!)
                    
                    automaticTankArray.append(enmy)
                    enmy.delegate = self
                    break
                }
            }
        }
    }
    

    到了这里重要的差不多讲完了。纯属学习哈,等那个gif转换好了我更新下哈。

    喜欢的话点个赞就好了哈,如果大家有什么好的建议也可以呼叫我哈!

    相关文章

      网友评论

          本文标题:iOS版本使用SpriteKit框架实现坦克大战游戏

          本文链接:https://www.haomeiwen.com/subject/ffgrpqtx.html