其他
在 iOS/SwiftUI 中用 SpriteKit 开发…
适用于需要基于 SpriteKit 构建 2D 游戏、交互动画并在 SwiftUI/UIKit 中呈现的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 39/100 ×0.15
适用场景
在 iOS/SwiftUI 中用 Spr特定
解决思路
- 通过 SKView 呈现 SKScene;SKScene 是框架每帧动画和渲染的节点树根节点。
- 子类化 SKScene 并重写生命周期方法;默认坐标原点在左下角。
- 在 didMove(to:) 中设置 backgroundColor、physicsWorld.contactDelegate、physicsBody = SKPhysicsBody(edgeLoopFrom: frame),并调用 setupNodes()。
- 重写 update(_:) 实现每帧逻辑,它在动作评估之前调用。
- 用 UIKit 呈现场景:guard let skView = view as? SKView else { return };设置 skView.ignoresSiblingOrder = true;创建 GameScene(size: skView.bounds.size);设置 scene.scaleMode = .resizeFill;调用 skView.presentScene(scene)。
- 按需求设置 scene.scaleMode:.resizeFill 适应视图尺寸变化,.aspectFill 用于固定设计游戏场景,.aspectFit 加黑边,.fill 拉伸可能变形。
- 在帧循环对应回调中实现逻辑:update(_:) → 评估动作 → didEvaluateActions() → 模拟物理 → didSimulatePhysics() → 应用约束 → didApplyConstraints() → didFinishUpdate();只重写需要工作的回调。
- 用 SKNode 作为不可见容器或布局组;子节点继承父节点位置、缩放、旋转、alpha 和 speed;用 SKSpriteNode 作为主要视觉节点。
- 按用途选择节点类型:SKSpriteNode 纹理图像或纯色,SKLabelNode 文本,SKShapeNode 矢量路径(每次绘制调用昂贵),SKEmitterNode 粒子,SKCameraNode 视口控制,SKTileMapNode 网格瓦片,SKAudioNode 位置音频,SKCropNode/SKEffectNode 遮罩/CIFilter,SK3DNode 嵌入 SceneKit 内容。
- 创建精灵:let player = SKSpriteNode(imageNamed: "hero");设置 player.position = CGPoint(x: frame.midX, y: frame.midY);设置 player.name = "player";调用 addChild(player)。
- 设置 skView.ignoresSiblingOrder = true 提升性能;然后用 zPosition 决定绘制顺序,如 background.zPosition = -1、player.zPosition = 0、foregroundUI.zPosition = 10;没有它则按节点树顺序绘制。
- 用 name 属性命名节点以便无需实例变量查找;用 childNode(withName:)、enumerateChildNodes(withName:using:) 或 subscript 查找;搜索模式 // 搜索整棵树,* 匹配任意字符,.. 指父节点。
- 用 SKAction 定义随时间应用于节点的变化;动作不可变且可重用;用 node.run(_:) 运行。
- 使用基本动作:SKAction.moveBy(x:y:duration:)、SKAction.scale(to:duration:)、SKAction.rotate(byAngle:duration:)、SKAction.fadeOut(withDuration:)、SKAction.removeFromParent()。
- 组合动作:SKAction.sequence([...]) 顺序执行,SKAction.group([...]) 并行执行,SKAction.repeatForever(...) 重复。
- 纹理动画:用 (1...8).map { SKTexture(imageNamed: "walk_\($0)") } 创建帧;用 SKAction.animate(with:timePerFrame:) 运行动画;用 SKAction.repeatForever(walkAction) 重复。
- 设置 timingMode 控制速度曲线(.linear、.easeIn、.easeOut、.easeInEaseOut);用 key 运行动作以便后续访问,如 player.run(pulse, withKey: "pulse");用 player.removeAction(forKey: "pulse") 停止。
- 用场景的 physicsWorld 管理重力和碰撞检测。
- 添加物理体:圆形用 SKPhysicsBody(circleOfRadius: player.size.width / 2) 并设置 restitution = 0.3;静态矩形用 SKPhysicsBody(rectangleOf: ground.size) 并设置 isDynamic = false;不规则形状用 SKPhysicsBody(texture: player.texture!, size: player.size)。
- 用位掩码控制碰撞和接触回调:定义 struct PhysicsCategory { static let player: UInt32 = 0b0001; static let enemy: UInt32 = 0b0010; static let ground: UInt32 = 0b0100 };设置 categoryBitMask 标识物体,collisionBitMask 控制物理响应(反弹),contactTestBitMask 触发 didBegin/didEnd。
- 实现 SKPhysicsContactDelegate 并在 didMove(to:) 中设置 physicsWorld.contactDelegate = self;在 didBegin(_:) 中合并掩码判断,如 let mask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask;接触回调在物理模拟期间运行,应让处理函数设置标志或追加事件,然后在 update(_:) 中应用节点/物理体/世界突变。
- 用 applyForce(CGVector(dx: 0, dy: 50)) 施加持续力;用 applyImpulse(CGVector(dx: 0, dy: 200)) 施加即时冲量;用 applyAngularImpulse(0.5) 施加旋转;用 applyImpulse 做跳跃和发射投射物;用 physicsWorld.gravity = CGVector(dx: 0, dy: -9.8) 配置重力,用 affectedByGravity 配置每物体重力。
- 在场景上重写 touchesBegan、touchesMoved、touchesEnded;用 nodes(at:) 命中测试,如 let location = touch.location(in: self);let tappedNodes = nodes(at: location);判断 tappedNodes.contains(where: { $0.name == "playButton" }) 后调用 startGame()。
- 节点级触摸处理:子类化节点并设置 isUserInteractionEnabled = true;该节点直接接收触摸而不是场景。
- 用 SKCameraNode 控制场景可见部分;将其作为子节点添加并赋值给 scene.camera,如 let cameraNode = SKCameraNode();addChild(cameraNode);camera = cameraNode;设置 cameraNode.position = CGPoint(x: frame.midX, y: frame.midY)。
- 跟随角色:在 didSimulatePhysics() 中设置 cameraNode.position = player.position,或用约束限制相机到世界边界:创建 SKRange(lowerLimit:upperLimit:),用 SKConstraint.positionX(xRange, y: yRange) 设置 cameraNode.constraints。
- 相机缩放和 HUD:反向缩放相机节点,setScale(0.5) 放大 2x,setScale(2.0) 缩小 2x;将 HUD 节点作为相机子节点添加,如 let scoreLabel = SKLabelNode(text: "Score: 0");设置位置、fontName = "AvenirNext-Bold"、fontSize = 24;cameraNode.addChild(scoreLabel)。
- 用 SKEmitterNode 生成粒子效果;在 Xcode 的 SpriteKit Particle File 编辑器中设计 .sks 文件或在代码中配置。
- 从文件加载发射器:guard let emitter = SKEmitterNode(fileNamed: "Fire") else { return };设置 emitter.position = CGPoint(x: frame.midX, y: 100);addChild(emitter)。
- 一次性发射器:设置 numParticlesToEmit 为有限值;完成后移除,如 explosion.numParticlesToEmit = 100;addChild(explosion);用 SKAction.wait(forDuration: TimeInterval(explosion.particleLifetime)) 和 SKAction.sequence([wait, .removeFromParent()]) 移除。
- 当发射器移动时,设置 emitter.targetNode = self 使粒子保持在场景世界空间。
- 用 SpriteView 在 SwiftUI 中嵌入 SpriteKit 场景,如 @State private var scene: GameScene = { let s = GameScene(); s.size = CGSize(width: 390, height: 844); s.scaleMode = .resizeFill; return s }();SpriteView(scene: scene).ignoresSafeArea()。
- 按需传入 SpriteView 选项:options: [.allowsTransparency] 透明背景,.shouldCullNonVisibleNodes 离屏剔除,.ignoresSiblingOrder 基于 zPosition 绘制顺序;开发时用 debugOptions: [.showsFPS, .showsNodeCount]。
- 在 SwiftUI 和场景之间通信:通过共享 @Observable 对象传递数据;将场景存储在 @State 中避免重新渲染时重新创建,如 @Observable final class GameState { var score = 0; var isPaused = false };@State private var gameState = GameState();@State private var scene = GameScene();SpriteView(scene: scene, isPaused: gameState.isPaused).onAppear { scene.gameState = gameState }。
- 避免在每次 SwiftUI 重新渲染时创建新场景;应使用 @State private var scene = GameScene(...) 创建一次并复用。
- 避免添加已有父节点的子节点;节点只能有一个父节点;先从当前父节点移除或创建单独实例,否则会崩溃。
- 避免只设置 categoryBitMask 而忘记设置 contactTestBitMask;两者都设置才能接收 didBegin/didEnd 接触回调。
- 避免在性能关键路径使用 SKShapeNode;它每个实例使用单独绘制调用;重复元素优先用带纹理的 SKSpriteNode 以启用批处理渲染。
- 避免让节点移出屏幕后不删除;应在动作序列中包含 .removeFromParent(),如 SKAction.sequence([SKAction.moveBy(x: -800, y: 0, duration: 3.0), SKAction.removeFromParent()])。
- 避免将 physicsWorld.contactDelegate = self 设置得太晚;应在 didMove(to:) 中设置,而不是 update(_:) 或延迟后。
- 检查 Scene 子类是否重写 didMove(to:) 进行设置而不是 init。
- 检查是否根据游戏设计适当选择 scaleMode。
- 检查是否在 SKView 上设置 ignoresSiblingOrder = true 以提升性能。
- 检查启用 ignoresSiblingOrder 时是否一致使用 zPosition。
- 检查是否在 didMove(to:) 中设置 Physics contactDelegate。
- 检查是否正确配置 category、collision 和 contact 位掩码。
- 检查是否为任何需要 didBegin/didEnd 回调的对设置 contactTestBitMask。
- 检查接触回调是否排队更改而不是直接修改物理世界。
- 检查静态物体是否使用 isDynamic = false。
- 检查是否在性能关键路径避免 SKShapeNode 并优先 SKSpriteNode。
- 检查将节点移出屏幕的动作是否包含 .removeFromParent() 在序列中。
- 检查一次性发射器是否在粒子生命周期结束后移除自身。
- 检查当粒子应留在世界空间时是否设置发射器 targetNode。
- 检查在 SwiftUI 中使用 SpriteView 时场景是否存储在 @State。
- 检查相关精灵是否使用纹理图集减少绘制调用。
- 检查 update(_:) 是否使用 delta time 实现帧率无关移动。
- 检查节点在重新添加到别处之前是否从父节点移除。
- 参考 references/spritekit-patterns.md 了解 tile maps、纹理图集、着色器、场景过渡、游戏循环模式、音频和 SceneKit 嵌入。
适用边界 · 注意事项
- 需要开发 3D 游戏或使用 SceneKit/RealityKit 的功能时,不适用。
- 需要跨平台发布到 Android、Web 或 Windows 时,不适用。
- 没有 Xcode/Apple 开发环境或无法使用 Swift/SwiftUI 时,不适用。