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

实战需求

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

实战效果

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
}

top Created with Sketch.