使用SpritKit编写一个简单的摇杆功能

关于SpritKit摇杆的制作

文章目录

效果展示

这里我们使用Apple原生的游戏引擎来进行一个2D游戏中我们摇杆的制作【当然3D的游戏摇杆的制作原理是差不多的】

场景的搭建

首先我们创建一个新的工程文件,将我们的用到的一些资源文件放到Assets图片管理中。(所有的资源文件将会分享出来)

gameImage1

好啦资源素材准备好,我们就来进行游戏场景的搭建吧!

1、创建游戏画布

创建SpriteKit Scene文件,它的后缀是sks。创建好之后我们可以看到这样一个画面

2、添加node和sprite

这里我们可以选择运用快捷键command + shift + L来找到我们的控件库,在控件库中我们找到一个叫做color sprite的控件,将它拖入到我们的游戏画布中,就可以看见如下效果:

这里我们可以看见它显示得很奇怪,后续我们需要做出一定的修改,我们目前先把我们需要用到的空间先拖放到我们的游戏画布中吧!

下面拖放的是node类型的控件

将它拖入到画布中。所有需要的控件拖完后我们的游戏画布的现实将会如图所示:

所有需要用到的控件都拖好了下面我们来将我们的游戏画布进行布局让它好看一点吧。

3、调整游戏画布

首先我们将我们的画布的大小改变一下,点击Scene这个文件名,然后调整它的W和H,以及添加它的Name​:warning:这一步很重要:bangbang:

我们将所有SKSpriteNode名字的文件中的color改成如下图所示。

并且将它们的名字改成如图所示:

下面就是资源的添加我们将每个Sprite类的文件中的text替换成我们的对应名称的图片。替换完后摆放位置后如下图所示:

还差最后一步我们的画面搭建才算完成!那就是给我们的小人添加物理属性(physics Definition),具体设置入下图所示:

解释一下一些参数吧:

Body Type:对物理主体的类型进行设置

Bounding rectangle边界矩形,物理主体是一个小人外套着的矩形,而不是在沿着小人的边缘

Bounding circle: 边界圆,物理主体是一个圆,即以小人为中心的一个圆。也不是沿着小人的边缘的

Alpha mask透明遮罩,这个可以沿着我们的形状边界勾勒出我们的一个物理主体。

Dynamic是否为物理物体添加动力

Allows Rotation: 指示物理物体是否受到施加于它的角力和脉冲的影响。

Pinned: 指示物理体的节点是否固定在其父节点上。

Affect By Gravity: 物理体是否收到重力影响

一些Mask:这些是一些掩码的设置。(如果想要具体了解的话可以去查一下)

这里就是定义哪些物理体可以物理体之间的进行交互【这里我还没做测试 可以先做简单的了解就好了】 下面的都链接到了官方文档里面可以自行查看!

Category Mask:定义物理体所属类别的掩码

Collision Mask: 定义碰撞检测掩码

Field Mask:它定义了哪些类别的物理场可以对该物理主体施加力。

Contact Mask: 定义接触检测掩码

:bangbang:提一下最关键的一步将我们的代码和游戏场景联合起来的操作是将我们的Scene的Class改成我们后面会创建的游戏的类的名称:bangbang:

到目前为止我们游戏的画面算是搭建好啦!下面就进入到代码环节啦!

游戏逻辑实现

1、小人的属性状态模型搭建

创建一个新的swift文件,我们这里将它命名为"playerStateMachine",这个文件里面写我们人物状态的模型的:

具体的代码如下:

import Foundation
import GameplayKit

//给我们的运动动画添加一个关键词(类似于ID)
fileprivate let characterAnimationkey = "Sprite Animation"

//MARK: - 玩家的状态属性

class PlayerState: GKState {
    //这里使用无主引用(其实和弱引用作用差不多避免强引用循环)
    unowned var playerNode: SKNode
    
    init(playerNode: SKNode) {
        self.playerNode = playerNode
        super.init()
    }
    
}

//MARK: -静止状态
class IdleState: PlayerState {
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        switch stateClass {
        case is LandingState.Type, is IdleState.Type:
            return false
        default:
            return true
        }
    }
    
    let textures = SKTexture(imageNamed: "player0")
    lazy var action = { SKAction.animate(with: [self.textures], timePerFrame: 0.1)}()
    
    override func didEnter(from previousState: GKState?) {
        playerNode.removeAction(forKey: characterAnimationkey)
        playerNode.run(action,withKey: characterAnimationkey)
    }
}


//MARK: -行走状态

class WalkingState: PlayerState {
    
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
        switch stateClass {
        case is LandingState.Type, is WalkingState.Type: return false
        default:
            return true
        }
    }
    
    let textures: Array<SKTexture> = (0..<6).map({return "player\($0)"}).map(SKTexture.init)
    
    lazy var action = {
        //永久循环
        SKAction.repeatForever(.animate(with: self.textures, timePerFrame: 0.1))
    }()
    
    override func didEnter(from previousState: GKState?) {
        playerNode.removeAction(forKey: characterAnimationkey)
        playerNode.run(action, withKey: characterAnimationkey)
    }
}

下面介绍一下几个关键的属性:

GKState:用于将特定于状态的逻辑定义为状态机的一部分的抽象超类【这里是一个抽象的概念 和我们的类是一个类似的概念】

下面以状态机来称呼它

SKNode: 就是我们的一个节点,这里我们可以把我们的场景内想象成一个个节点组成的

isValidNextState: 返回一个布尔值,该值指示当前处于此状态的状态机是否允许转换到指定的状态[就是是否去进行状态的转换这个是我们GameplayKit自带的方法]。 如果允许转换到指定状态,则为true;否则为false。

didEnter(from:):当状态机转换到此状态时,执行自定义操作。【即我们状态转换后执行的一个操作】

这里我们先去定义一个名叫PlayerState基础的状态类,这里我们是以小人为基础类,其他的属性都是继承这个类的,可以把这个整体理解为一棵树,我们的小人是树的主体,其他的属性是树的枝叶。

这里我们可以看见每一个属性其实代码是差不多的,我们就拿静止状态和运动状态来说吧!!!

//继承PlayerState这个类
class WalkingState: PlayerState {
    //进行一个状态转换的判断【这里可以理解为我们的一个模板】
    override func isValidNextState(_ stateClass: AnyClass) -> Bool {
      //判断当前的状态是否执行完毕【这里是:如果着陆状态和行走状态正在进行的话那么我们就不去切换当前的一个状态,如果这两个动作其中一个完成了我们就切换状态】
        switch stateClass {
        case is LandingState.Type, is WalkingState.Type: return false
        default:
            return true
        }
    }
    
  //这里我们进行一个逐帧动画的操作,即每一帧都进行图片的切换形成我们的一个逐帧动画将其添加到数组中
    let textures: Array<SKTexture> = (0..<6).map({return "player\($0)"}).map(SKTexture.init)
    
  
  //设置我们的一个SKAction动作,这里repeatForever是永久循环的一个状态。这里是我们每0.1秒切换我们的图片,一直循环(即循环完了就从头开始循环)
    lazy var action = {
        //动作永久循环
        SKAction.repeatForever(.animate(with: self.textures, timePerFrame: 0.1))
    }()
    
  //这里是我们当前状态要去执行的一些状态动作
    override func didEnter(from previousState: GKState?) {
      //我们将之前的动作进行一个清空,然后在执行我们当前这个状态想要执行的动作【即循环播放我们的动画】这里的key设置一样是为了我们的修改方便
        playerNode.removeAction(forKey: characterAnimationkey)
        playerNode.run(action, withKey: characterAnimationkey)
    }
}

2、游戏画面对应节点的绑定

状态机创建完后我们开始来绑定我们游戏画面的节点吧!具体代码如下

import Foundation
import SpriteKit
import GameplayKit

//创建一个游戏的场景类,该类里面存储游戏场景中的元素
class GameScene: SKScene, SKPhysicsContactDelegate {
    
    //MARK: -声明nodes
    //人物
    var player: SKNode?
    //操作杆
    var joystick: SKNode?
    //操作杆的把手
    var joystickKnob: SKNode?
    //相机
    var cameraNode: SKCameraNode?
    
    
    //创建对应的action
    var joystickAction = false
    
    //固定旋钮的运动半径
    var knobRadius: CGFloat = 50.0
    
    
    //给游戏一些基础设置
    var previousTimeInterval: TimeInterval = 0
    //人物朝右
    var playerIsfacingRight = true
    // 人物运动的速度
    let playerSpeed = 4.0
    
    //人物装载属性的集合
    var playerStateMachine: GKStateMachine!
    
    override func didMove(to view: SKView) {
        
        //添加物理属性代理
        physicsWorld.contactDelegate = self
        
        player  = childNode(withName: "player")
        joystick = childNode(withName: "joystick")
        joystickKnob = joystick?.childNode(withName: "knob")
        cameraNode = childNode(withName: "cameraNode") as? SKCameraNode
        
        //给player添加状态
        playerStateMachine = GKStateMachine(states: [
            WalkingState(playerNode: player!),
            IdleState(playerNode: player!),
            LandingState(playerNode: player!)
        ])
        
        playerStateMachine.enter(IdleState.self)
    }
    
}

//MARK: -一些触摸类型的扩展
extension GameScene {
    //MARK: - 关于摇杆的触碰
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        NSLog("xixixi")
      //进行一个我们点击点的循环
        for touch in touches {
            if let joystickKnob = joystickKnob {
              //这里设置我们的位置处于joystickKnob的位置单位内,这里的in后面要放的就是我们想要去点击对象是谁,返回该对象中对应的坐标
                let location = touch.location(in: joystickKnob)
                joystickAction = joystickKnob.frame.contains(location)
                NSLog("xixixi")
            }
            
  
            NSLog("lalalal")
        }
    }
    
    //MARK: - 移动摇杆
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        //确保值不为空
        NSLog("lalalal")//这里的NSLog是我们平时调试的时候会用到的它会比print来调试要好,它会指出我们的代码处于的一个位置
        guard let joystick = joystick else {return}
        guard let joystickKnob = joystickKnob else {return}
        
        if !joystickAction {return}
        
        //计算移动的距离
        for touch in touches {
            let position = touch.location(in: joystick)
            
            //通过勾股定理来计算[这里是考虑到玩家的移动不一定是水平移动摇杆为此用勾股定理来解决这个问题]
            let length = sqrt(pow(position.y, 2) + pow(position.x, 2))
            
            let angle = atan2(position.y, position.x)
            
            if knobRadius > length {
                joystickKnob.position = position
            } else {
                joystickKnob.position = CGPoint(x: cos(angle) * knobRadius, y: sin(angle) * knobRadius)
                NSLog("xixixi")
            }
        }
    }
    
    //MARK: - 移动结束,摇杆返回中间位置
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        NSLog("lalalal")
        for touch in touches {
            //获取x坐标
            let xJoystickCoordinate = touch.location(in: joystick!).x
            //限制最大的移动
            let xLimit: CGFloat = 200.0
            if xJoystickCoordinate > -xLimit && xJoystickCoordinate < xLimit{
            resetKnobPosition()
            }
        }
        
    }
}


//MARK: - Action
extension GameScene {
  //创建复位动作函数(这里的所有的位置都是相对位置哦!)
    func resetKnobPosition() {
        let initialPoint = CGPoint(x: 0, y: 0)
        let moveBack = SKAction.move(to: initialPoint, duration: 0.1)
        moveBack.timingMode = .linear
      //摇杆节点执行复位操作
        joystickKnob?.run(moveBack)
      //此刻摇杆节点的动作状态切换到fale
        joystickAction = false
    }
}

//MARK: -角色移动刷新每一帧
extension GameScene {
  //这里是刷新的时间。部分代码块可以当作模板来使用
    override func update(_ currentTime: TimeInterval) {
        let deltaTime = currentTime - previousTimeInterval
        previousTimeInterval = currentTime
        
        //相机的移动
        cameraNode?.position.x = player!.position.x
      //这里减去数值可以自己尝试这去慢慢调试,这里并不是固定的值
        joystick?.position.y = (cameraNode?.position.y)! - 100.0
        joystick?.position.x = (cameraNode?.position.x)! - 300.0
        
        //角色移动
        guard let joyStickKnob = joystickKnob else {return}
        let xPosition = Double(joyStickKnob.position.x)
      //这里运用一个三元运算符
        let positivePosition = xPosition < 0 ? -xPosition : xPosition
        if floor(positivePosition) != 0 {
            playerStateMachine.enter(WalkingState.self)
        }
        else
        {
            playerStateMachine.enter(IdleState.self)
        }
        let displacement = CGVector(dx: deltaTime * xPosition * playerSpeed, dy: 0)
        let move = SKAction.move(by: displacement, duration: 0)
        let faceAction:SKAction!
        let movingRight = xPosition > 0
        let movingLeft = xPosition < 0
        if movingLeft && playerIsfacingRight {
            playerIsfacingRight = false
            let faceMovement = SKAction.scaleX(to: -1, duration: 0.0)
            faceAction = SKAction.sequence([move,faceMovement])
            
        } else if movingRight && !playerIsfacingRight{
            playerIsfacingRight = true
            let faceMovement = SKAction.scaleX(to: 1, duration: 0.0)
            faceAction = SKAction.sequence([move,faceMovement])
        }else{
            faceAction = move
        }
        player?.run(faceAction)
        
    }
}

最后再将我们画面添加到我们到入口代码中吧,跳转到ViewController这个文件中将代码替换成下面代码:

//
//  ViewController.swift
//  SpritKitTestOne
//
//  Created by 韦小新 on 2022/11/24.
//

import UIKit
import SpriteKit
import GameplayKit

class GameViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let view = self.view as! SKView? {
            // Load the SKScene from 'GameScene.sks'
            if let scene = SKScene(fileNamed: "GameScene") {
                // Set the scale mode to scale to fit the window
                scene.scaleMode = .aspectFill
                
                // Present the scene
                view.presentScene(scene)
                
            }
            
            view.ignoresSiblingOrder = false
            
            view.showsFPS = true
            view.showsNodeCount = true
        }
    }

    override var shouldAutorotate: Bool {
        return true
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }
}

好啦到了最关键的一步,如果不做这一步你的程序将会出现一个莫名其妙的报错。这里我们到我们的Main.Storyboard文件中,将我们的View的class切换成SKView即可运行我们的项目啦!

demo下载地址

4 个赞

可以把最终的实现效果录个gif放在帖子开头吗

1 个赞

呀 编辑的时候忘记加上去啦! 现在已经加上去啦:rose:

1 个赞