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

开场白:

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

额,因为gif图转换太慢了,后期在补上,先上传几张图片看看效果哈!
iOS版本使用SpriteKit框架实现坦克大战游戏_第1张图片
开始前
iOS版本使用SpriteKit框架实现坦克大战游戏_第2张图片
开战中
iOS版本使用SpriteKit框架实现坦克大战游戏_第3张图片
开战中
iOS版本使用SpriteKit框架实现坦克大战游戏_第4张图片
选择难易程度
看到效果图,如果感觉还行,有必要继续看的童鞋继续哈,下面针对比较重要且有难点的地方讲解一下了。

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

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

这个是游戏手柄部分代码

/**
 游戏控制手柄
 */
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 = []

///先移动到和目标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 = []
    ///持有场景对象
    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 {
        
        var list:Array = []
        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框架实现坦克大战游戏)