Advertisement

SwiftUI 游戏开发之 乒乓球游戏基于SpriteKit SwiftUI Combine (教程含源码)

阅读量:

实战需求

SwiftUI 游戏开发之 乒乓球游戏基于SpriteKit SwiftUI Combine

实战效果

SwiftUI 游戏开发之 乒乓球游戏基于SpriteKit SwiftUI Combine
SwiftUI 游戏开发之 乒乓球游戏基于SpriteKit SwiftUI Combine

解决方案

乒乓球游戏与树一样古老的游戏,可追溯到 1972 年。万一你出生在这个世纪而不是最后一个世纪,让我解释一下它是什么。这是一款两人游戏[在当时很不寻常],画面非常简单。你控制一个桨,另一个球员控制另一个,你的球被你从一边敲到另一边。这是一场电子乒乓球比赛。

然后回到编码。总的来说,我喜欢 SpriteKit 和 SwiftUI 之间的协同作用。后者非常适合安排/组织你的屏幕——它的布局机制与 VStacks、HStacks 甚至网格超级容易做到——前者,SpriteKit 非常深思熟虑的基本组件集合,你需要构建一个好的 2D 游戏。

构建

我使用物理世界的重力原语来驱动球(我称之为岩石)。在这个版本中,我需要球来回移动,所以我使用了 SKAction 指令来给出开始游戏的“冲动”;除此之外,我依赖物理对象原语,以便它四处弹跳。
另一个重大变化是使用 SpriteKit 作为控制面来管理我的 SpriteKit 桨。我本可以使用单个控制/显示表面,但我想保留在游戏中使用 SwiftUI 布局机制的选项。我使用 SpriteKit 作为控制和显示表面,因为 SwiftUI 速度不够快。SwiftUI 适合静态内容和布局,SpriteKit 动态内容。我在拨片上留下了一个分数计数器,以在此处用斜体字说明所述点。您在玩的过程中会看到,SwiftUI 根本不够快。
我需要做这个奇怪的数学方程来这样做,因为 SwiftUI 坐标中心虽然 X 轴上的相同在 Y 上相反?我不知道苹果为什么这样做——我相信这是有充分理由的。
看起来像这样的 SwiftUI 代码有点奇怪。

复制代码
    .onReceive(leftPaddle) { ( point ) in 
      let ry = 256 - point.y 
      let np = CGPoint(x: point.x, y: ry) 
      marker = np 
    } 
    .overlay(Text(returnText()) 
    .font(.标注) 
    .foregroundColor(Color.white) 
    .position(marker))
    
    
      
      
      
      
      
      
      
      
      
    
    代码解读

我想过创建一个委托协议来让三个 SpriteKit 场景相互交谈,实际上是 SwiftUI 代码,但我决定尝试使用组合框架来处理消息,我做到了。在大多数情况下运行良好的解决方案。
我需要一个动作来取回我的球,如果它以某种方式出局并解决了摇动手势,这就是为什么你会在内容视图中找到它的扩展。
我最初使用球的坐标来确定它是否不在比赛中,但它每次都会再次出现一个 Sprite 擅离职守,所以我不得不想出一个新计划,因此更多的 Sprite 来捕捉场外事件. 你看不到它们,因为它们是黑色的。
我使用 SwiftUI 开始游戏,在结束时显示分数并确实根据要求重新启动游戏。我也用它来标记我的球场。所有感觉比在纯 SpriteKit 环境中工作量更少的任务。
当球员击球时,我再次使用“脉冲”将球推向一侧,以确保它不会在桨之间以 90 度角撞击。
最后已经提到我使用参数设置球的物理体从边缘和桨上反弹。您需要调整以使游戏更快的参数是这些参数。注释是弹力。

复制代码
    rock.physicsBody!.affectedByGravity = false
    rock.physicsBody!.restitution = 0.8 //弹力
    rock.physicsBody!.linearDamping = 0
    rock.physicsBody!.friction = 0.3
    rock.physicsBody?.isDynamic = true
    rock.physicsBody!.mass = 0.5
    rock.physicsBody!.allowsRotation = true
    
    
      
      
      
      
      
      
      
    
    代码解读

项目代码

主界面

复制代码
    //
    //  ContentView.swift
    //  gameIII
    //
    //  Created by localadmin on 24.06.21.
    //
    import SwiftUI
    import SpriteKit
    import Combine
    
    let newGame = PassthroughSubject<CGPoint,Never>()
    let leftPaddle = PassthroughSubject<CGPoint,Never>()
    let rightPaddle = PassthroughSubject<CGPoint,Never>()
    let scored = PassthroughSubject<CGPoint,Never>()
    let endOfGame = PassthroughSubject<(Int,Int),Never>()
    
    
    struct ContentView: View {
      var scene = GameScene()
      var leftPaddleControl = CtrlScene()
      var rightPaddleControl = CtrlScene()
      
      init() {
    scene.size = CGSize(width: 384, height: 256)
    scene.scaleMode = .fill
    leftPaddleControl.size = CGSize(width: 64, height: 256)
    leftPaddleControl.scaleMode = .fill
    rightPaddleControl.size = CGSize(width: 64, height: 256)
    rightPaddleControl.scaleMode = .fill
    leftPaddleControl.name = "leftPaddle"
    rightPaddleControl.name = "rightPaddle"
      }
      @State var marker = CGPoint.zero
      @State private var message = ""
      @State var gameOn = "Ready"
      
      var body: some View {
    ZStack {
      
      HStack {
        
        SpriteView(scene: leftPaddleControl)
          .frame(width: 64, height: 256)
          .ignoresSafeArea()
          .mask(RoundedRectangle(cornerSize: CGSize(width: 16, height: 16)).frame(width: 64, height: 256))
        
        
        
        SpriteView(scene: scene)
          .frame(width: 384, height: 256)
          .ignoresSafeArea()
          .mask(RoundedRectangle(cornerSize: CGSize(width: 16, height: 16)).frame(width: 384, height: 256))
          
          .onReceive(leftPaddle) { ( point ) in
            let ry = 256 - point.y
            let np = CGPoint(x: point.x, y: ry)
            marker = np
          }
          .onReceive(rightPaddle) { ( point ) in
            let ry = 256 - point.y
            let np = CGPoint(x: point.x, y: ry)
            marker = np
          }
          .onReceive(scored) { ( point ) in
            let ry = 256 - point.y
            let np = CGPoint(x: point.x, y: ry)
            marker = np
          }
          .onReceive(NotificationCenter.default.publisher(for: .deviceDidShakeNotification)) { _ in
            scene.newBall = true
          }
          
          .onReceive(newGame) { ( point ) in
            scene.newSprite = true
          }
          .onReceive(rightPaddle) { ( point ) in
            scene.rightSprite = point
          }
          .onReceive(leftPaddle) { ( point ) in
            scene.leftSprite = point
          }
          .overlay(Text(returnText())
                    .font(.callout)
                    .foregroundColor(Color.white)
                    .position(marker))
        
        SpriteView(scene: rightPaddleControl)
          .frame(width: 64, height: 256)
          .ignoresSafeArea()
          .mask(RoundedRectangle(cornerSize: CGSize(width: 16, height: 16)).frame(width: 64, height: 256))
        
        
      }
      VStack {
        Text(message)
          .font(.headline)
          .foregroundColor(Color.yellow)
          .frame(width: 128, height: 32, alignment: .center)
          
          .onReceive(endOfGame) { ( Scores ) in
            let (leftWin,rightWin) = Scores
            message = "GameOver \(leftWin):\(rightWin)"
            gameOn = "Replay"
          }
        if scene.gameOver {
          Button {
            scene.newSprite = true
            scene.gameOver = false
            scene.leftWins = 0
            scene.rightWins = 0
            message = ""
          } label: {
            Text(gameOn)
              .frame(width: 128, height: 32, alignment: .center)
          }
        }
        
      }
      ZStack {
        Circle()
          .stroke(Color.white, lineWidth: 1)
          .frame(width: 96, height: 96)
          .opacity(0.5)
      }
      Rectangle()
        .stroke(Color.white, lineWidth: 1)
        .frame(width: 2, height: 256)
        .opacity(0.5)
    }
      }
      func returnText() -> String {
    if scene.leftWins > 1 || scene.rightWins > 1 {
      return ""
    }
    if scene.leftWins > 0 || scene.rightWins > 0 {
      return "\(scene.leftWins):\(scene.rightWins)"
    }
    return ""
      }
    }
    
    extension NSNotification.Name {
      public static let deviceDidShakeNotification = NSNotification.Name("MyDeviceDidShakeNotification")
    }
    
    extension UIWindow {
      open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
    super.motionEnded(motion, with: event)
    NotificationCenter.default.post(name: .deviceDidShakeNotification, object: event)
      }
    }
    
    struct ContentView_Previews: PreviewProvider {
      static var previews: some View {
    ContentView()
      }
    }
    
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

组件代码

复制代码
    import SpriteKit
    import Combine
    
    class CtrlScene: SKScene {
      
      var box:SKSpriteNode!
      var rock:SKSpriteNode!
      var dragOffset = CGSize.zero
      
      override func didMove(to view: SKView) {
    physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
      }
      
      override func update(_ currentTime: TimeInterval){
    if dragOffset.height != 0 {
      box?.position.y -= dragOffset.height / 10
      if box!.position.y < 0 {
        box?.position.y = 256
      }
      if box!.position.y > 256 {
        box?.position.y = 0
      }
      dragOffset.height = 0
    }
      }
      
      func nextBox( location:CGPoint ) {
    box = SKSpriteNode(color: SKColor.darkGray, size: CGSize(width: 48, height: 48))
    
    box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 48, height: 48))
    box.physicsBody?.affectedByGravity = false
    //        box.physicsBody?.isDynamic = false
    box.physicsBody!.contactTestBitMask = box.physicsBody!.collisionBitMask
    box.position = location
    box.name = self.name
    addChild(box)
      }
      
      override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    if box == nil {
      let location = touch.location(in: self)
      let newLocation = CGPoint(x: 32, y: location.y)
      nextBox(location: newLocation)
    }
      }
      
      override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let location = touch.location(in: self)
    let newLocation = CGPoint(x: 32, y: location.y)
    box.position = newLocation
    
    if box.name == "leftPaddle" {
      let leftLocation = CGPoint(x: 24, y: location.y)
      leftPaddle.send(leftLocation)
    }
    if box.name == "rightPaddle" {
      let rightLocation = CGPoint(x: 360, y: location.y)
      rightPaddle.send(rightLocation)
    }
      }
    }
    
    
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

游戏逻辑

复制代码
    import SpriteKit
    import Combine
    
    class GameScene: SKScene {
      
      var leftBox:SKSpriteNode!
      var rightBox: SKSpriteNode!
      var rock:SKSpriteNode!
      var gameOver: Bool = true
      var newSprite: Bool? = nil
      var leftSprite: CGPoint? = nil
      var rightSprite: CGPoint? = nil
      var rightWins = 0
      var leftWins = 0
      var lastPoint:String!
      var newBall: Bool = false
      
      override func didMove(to view: SKView) {
    
    let newFrame = CGRect(x: -64, y: 0, width: self.frame.size.width + 128, height: self.frame.size.height)
    
    physicsBody = SKPhysicsBody(edgeLoopFrom: newFrame)
    physicsBody?.friction = 0.0
    physicsBody?.restitution = 1.0
    physicsBody?.linearDamping = 0.0
    physicsBody?.angularDamping = 0.0
    physicsBody?.affectedByGravity = false
    physicsWorld.contactDelegate = self
    
    let leftLine = SKSpriteNode(color: SKColor.black, size: CGSize(width: 2, height: 512))
    leftLine.position = CGPoint(x: 16, y: 0)
    leftLine.name = "leftLine"
    leftLine.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 2, height: 512))
    leftLine.physicsBody?.affectedByGravity = false
    leftLine.physicsBody?.isDynamic = false
    leftLine.physicsBody?.contactTestBitMask = leftLine.physicsBody!.collisionBitMask
    addChild(leftLine)
    
    let rightLine = SKSpriteNode(color: SKColor.black, size: CGSize(width: 2, height: 512))
    rightLine.position = CGPoint(x: 370, y: 0)
    rightLine.name = "rightLine"
    rightLine.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 2, height: 512))
    rightLine.physicsBody?.affectedByGravity = false
    rightLine.physicsBody?.isDynamic = false
    rightLine.physicsBody?.contactTestBitMask = rightLine.physicsBody!.collisionBitMask
    addChild(rightLine)
      }
      
      override func update(_ currentTime: TimeInterval){
    
    if newSprite != nil {
      leftBox = nextBox(location: CGPoint(x: 24, y: 64), color: SKColor.white)
      rightBox = nextBox(location: CGPoint(x: 360, y: 64), color: SKColor.white)
      nextRock()
      newSprite = nil
    }
    if leftSprite != nil {
      if leftBox != nil {
        leftBox.position = leftSprite!
      }
      leftSprite = nil
    }
    if rightSprite != nil {
      if rightBox != nil {
        rightBox.position = rightSprite!
      }
      rightSprite = nil
    }
    if newBall {
      nextRock()
      newBall = false
    }
    
      }
      
      func nextRock() {
    let rnd = CGFloat(Int.random(in: 0...128))
    rock = SKSpriteNode(color: SKColor.white, size: CGSize(width: 10, height: 10))
    rock.position = CGPoint(x: 192, y: rnd)
    rock.name = "rock"
    rock.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 10, height: 10))
    rock.physicsBody!.affectedByGravity = false
    rock.physicsBody!.restitution = 0.8
    rock.physicsBody!.linearDamping = 0
    rock.physicsBody!.friction = 0.3
    rock.physicsBody?.isDynamic = true
    rock.physicsBody!.mass = 0.5
    rock.physicsBody!.allowsRotation = true
    if lastPoint == "left" {
      self.rock.position = CGPoint(x: 300, y:rightBox.position.y)
    }
    if lastPoint == "right" {
      self.rock.position = CGPoint(x: 64, y:leftBox.position.y)
    }
    addChild(rock)
    let delay = SKAction.wait(forDuration: 2)
    if lastPoint == "left" {
      let boost = SKAction.run({
        self.rock.physicsBody?.applyImpulse(CGVector(dx: -30.0, dy: 0.0))
      })
      let action = SKAction.group([delay,boost])
      rock.run(action)
    } else {
      let boost = SKAction.run({
        self.rock.physicsBody?.applyImpulse(CGVector(dx: 30.0, dy: 0.0))
      })
      let action = SKAction.group([delay,boost])
      rock.run(action)
    }
      }
      
      func nextBox( location:CGPoint, color: SKColor) -> SKSpriteNode {
    let box = SKSpriteNode(color: SKColor.white, size: CGSize(width: 4, height: 48))
    box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 4, height: 48))
    box.physicsBody?.affectedByGravity = false
    box.physicsBody?.isDynamic = false
    box.physicsBody?.contactTestBitMask = box.physicsBody!.collisionBitMask
    box.physicsBody!.restitution = 1.6
    box.position = location
    box.name = "box"
    addChild(box)
    return box
      }
      
    }
    
    extension GameScene: SKPhysicsContactDelegate {
      
      func didBegin(_ contact: SKPhysicsContact) {
    guard let nodeA = contact.bodyA.node else { return }
    guard let nodeB = contact.bodyB.node else { return }
    
    print("nodeAB ",nodeA,nodeB)
    
    if nodeA.name == "rock" {
      collisionBetween(ball: nodeA, object: nodeB)
    }
    if nodeB.name == "rock" {
      collisionBetween(ball: nodeB, object: nodeA)
    }
    if nodeA.name == "rightLine" {
      collisionBetween(ball: nodeA, object: nodeB)
      leftWins += 1
      destroy(ball: nodeB)
      lastPoint = "left"
    }
    if nodeA.name == "leftLine" {
      collisionBetween(ball: nodeA, object: nodeB)
      rightWins += 1
      destroy(ball: nodeB)
      lastPoint = "right"
    }
      }
      
      func collisionBetween(ball: SKNode, object: SKNode) {
    
    if object.name == "box" {
      scored.send(object.position)
      let rnd = Double.random(in: -8...8)
      let boost = SKAction.run({
        self.rock.physicsBody?.applyImpulse(CGVector(dx: 0, dy: rnd))
      })
      let action = SKAction.group([boost])
      rock.run(action)
    }
      }
      
      func destroy(ball: SKNode) {
    rock.removeFromParent()
    if (leftWins > 1 || rightWins > 1) {
      if !gameOver {
        endOfGame.send((leftWins,rightWins))
        leftBox.removeFromParent()
        rightBox.removeFromParent()
        gameOver = true
      }
    }
    if !gameOver {
      nextRock()
    }
      }
    }
    
    
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    
    代码解读

项目文件

186swiftui_demo_b.zip
链接:https://pan.baidu.com/s/1UmX_hbhE0yjGbw11xR6kSg 密码:d9iv

加入我们一起学习SwiftUI

QQ:3365059189
SwiftUI技术交流QQ群:518696470
教程网站:www.openswiftui.com

全部评论 (0)

还没有任何评论哟~