# MotionGame **Repository Path**: sgyk/motion-game ## Basic Information - **Project Name**: MotionGame - **Description**: 体感游戏 - **Primary Language**: Swift - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-18 - **Last Updated**: 2026-09-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MotionGame · iOS 摄像头体感游戏 一个**纯原生 iOS** 的摄像头体感游戏原型。用 iPhone 前置摄像头识别你的身体动作, 抬手去碰屏幕上飘起来的泡泡。 不依赖任何第三方库、不需要下载模型、不联网。 识别用的是苹果自带的 **Vision framework**(`VNDetectHumanBodyPoseRequest`), 19 个关节,Neural Engine 端侧推理,**摄像头画面从头到尾不出这台设备**。 --- ## 怎么跑起来 1. 双击 `MotionGame.xcodeproj` 打开 Xcode。 2. 左侧选中 `MotionGame` target → **Signing & Capabilities** → Team 选你自己的 Apple ID(免费的就行),Bundle Identifier 改成唯一的, 比如 `com.你的名字.motiongame`。 3. 用数据线把 iPhone 插上,Xcode 顶部的运行目标从模拟器换成你的 iPhone。 4. `⌘R`。 第一次运行 iPhone 会弹「不受信任的开发者」,去 **设置 → 通用 → VPN 与设备管理** 里信任一下你自己的证书即可。 免费 Apple ID 签名的 App 有效期 7 天,过期重新 `⌘R` 一次就行。 ### 为什么一定要真机 **iOS 模拟器没有摄像头。** 这不是配置问题,是模拟器根本不提供摄像头设备。 在模拟器里运行会看到「找不到可用的摄像头」的提示,这是预期行为。 想快速验证 UI 和编译是否正常,模拟器是可以的;要玩体感必须真机。 --- ## 代码结构 ``` MotionGame/ ├── MotionGameApp.swift 入口 ├── GameView.swift SwiftUI 界面:相机预览 + 游戏层 + HUD + 调试面板 ├── GameSession.swift 总线:把相机、识别、滤波、手势、游戏串起来 ├── Camera/ │ ├── CameraCaptureService.swift AVCaptureSession,前置摄像头 720p @30fps │ └── CameraPreviewView.swift 把 AVCaptureVideoPreviewLayer 包成 SwiftUI 视图 ├── Vision/ │ ├── PoseDetector.swift Vision 全身姿态 → 19 个关节 │ ├── OneEuroFilter.swift 1€ 滤波器:去抖但不延迟 │ └── GestureEngine.swift 关节坐标 → 游戏输入(含举手/倾斜/下蹲判断,现成的) └── Game/ └── BubbleScene.swift SpriteKit 泡泡游戏 + 骨架可视化 ``` 数据流: ``` 摄像头 30fps → AVCaptureVideoDataOutput(原始帧,不旋转不镜像) → Vision PoseLandmarker(orientation 决定怎么摆正,见下) → 置信度过滤(< 阈值丢弃,否则会有「幽灵手臂」) → 1€ 滤波(去抖) → GestureEngine(把手腕沿手肘方向外推,近似手掌位置) → BubbleScene(碰撞判定 + 渲染) ``` --- ## 调试面板 点右上角滑块图标打开。**因为摄像头的方向处理跟具体设备和握持方式有关, 我把可能出问题的参数全做成了可实时调、不用重新编译的旋钮。** | 参数 | 作用 | |---|---| | 手掌大小 | 手掌判定半径。孩子手臂短、挥动幅度小,调大更容易拍到 | | 手臂伸长 | 手腕往外延伸多少。Vision 只给到手腕,没有手指,靠这个补 | | 泡泡速度 | 调慢一点更适合小小孩 | | 识别灵敏度 | 关节置信度阈值。老是丢手就调低,画面里出现乱飘的点就调高 | | 显示骨架 | 打开能看到识别到的骨骼线,排查问题必开 | | 识别方向 | **如果骨架上下颠倒或左右反了,在这里换一个** | ### 关于「识别方向」 摄像头返回的帧是横向的传感器原始数据,要摆正需要旋转 + 镜像。 代码里默认前置摄像头竖屏用 `leftMirrored`(这是最常用的值), 但我**没法在模拟器上验证真实摄像头的输出方向**——模拟器没有摄像头。 所以:真机上如果发现方向不对,打开调试面板,在下面那排按钮里挨个试 (`leftMirrored` / `rightMirrored` / `up` / `upMirrored` …), 哪个对了就记住,回来改 `GameSession.swift` 里的默认值: ```swift @Published var orientation: FrameOrientation = .leftMirrored // 改成试出来的那个 ``` 调对了就是一行的事。 --- ## 想加新玩法? 这个工程的价值主要在**地基**:相机采集、姿态识别、滤波、坐标映射都已经处理好了。 加玩法基本只需要动两个地方: **1. 在 `GestureEngine.swift` 里加一个手势判断。** 比如: ```swift /// 手臂是否向左侧平伸(判断方向闪避用) func armExtendedLeft(_ joints: [BodyJoint: CGPoint]) -> Bool { guard let lw = joints[.leftWrist], let ls = joints[.leftShoulder] else { return false } return abs(lw.y - ls.y) < 0.08 && lw.x < ls.x - 0.15 } ``` 原理都一样:**比较关节坐标**。举手是手腕 y 小于肩膀 y,下蹲是髋部 y 低于基线, 侧身是肩线斜率超阈值。游戏从来不是「看到」你,它只是在读坐标。 **2. 换掉 `Game/BubbleScene.swift`。** 它只接收两个东西: ```swift scene.handPoints = [...] // 归一化屏幕坐标的手,原点左上 scene.jointPoints = [...] // 所有关节,画骨架用 ``` 把碰撞判定和渲染换掉,接水果、打地鼠、模仿动作、舞蹈打分都能长出来。 --- ## 已知取舍 - **泡泡游戏刻意没有失败机制**:没有计时、没有生命值、飘走了就换一个。 目标年龄段是 3–6 岁,唯一的反馈是正向的。 - **骨架用 `SKShapeNode` 每帧重建 path**:关节数少(十几个),开销可以忽略, 换来的是可视化调试非常直观。 - **`VNDetectHumanBodyPoseRequest` 只给到手腕,没有手指。** 想要捏合、比数字这类手势,需要换 `VNDetectHumanHandPoseRequest`(21 个手部关节), 代价是需要单独跑一路识别,性能开销明显上升。 - **上半身优先**:孩子个子矮,画面里通常看不到腿, `PoseDetector` 对下半身关节缺失是容忍的(只要求至少 3 个关节有效)。