首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

Kinect for Windows SDK 開發入門(八):骨骼追蹤進階(上)─ 將 Kinect 視為 WPF 輸入裝置與西蒙說遊戲實作

     編輯:關於.NET

前言

前七篇文章奠定了 Kinect 感測器(彩色、深度、紅外)的基礎操作。本文進入骨骼追蹤進階階段,核心主題是:如何將 Kinect 視為 WPF 應用程式的原生輸入裝置,並透過一個完整的「西蒙說」遊戲實作,示範命中測試、視覺回饋、遊戲狀態機等關鍵技術。

一、 WPF 輸入系統與 Kinect 整合觀念

1.1 標準輸入裝置 vs. Kinect

WPF 內建完整的輸入系統(System.Windows.Input),原生支援滑鼠、鍵盤、觸控筆、觸控板。輸入管理器(InputManager)負責將裝置座標轉換為 UI 元素座標,並觸發 MouseEnter、MouseLeftButtonDown 等路由事件。Kinect 尚未整合進 WPF 核心,因此開發者必須自行實作:

  • 從 SkeletonFrameReady 取得關節點座標。
  • 將骨骼座標系映射至 UI 座標系(MapSkeletonPointToDepth + 縮放)。
  • 自行執行命中測試(VisualTreeHelper.HitTest 或 UIElement.InputHitTest)。
  • 手動切換 UI 元素視覺狀態(懸停、按下、釋放)。

1.2 命中測試基礎

WPF 提供兩種主要命中測試 API:

方法適用場景回傳值
VisualTreeHelper.HitTest(visual, point)通用視覺樹測試最上層 DependencyObject
UIElement.InputHitTest(point)僅需 IInputElement(如 Button、TextBox)IInputElement

關鍵步驟:

  1. 關節點骨骼座標 → LayoutRoot 座標(GetJointPoint)。
  2. LayoutRoot 座標 → 目標元素局部座標(TranslatePoint)。
  3. 呼叫目標元素 InputHitTest,非 null 即命中。

1.3 互動模式差異比較

特性滑鼠觸控Kinect 骨骼
互動點數1多點(單一主點)最多 20 關節點 × 2 玩家
Z 軸資訊無接觸面積真實深度(米)
點擊等價動作按鍵觸控抬起需自訂手勢/姿勢
懸停偵測原生原生需自行實作

本文採用「雙手各自獨立追蹤,僅手部關節點參與 UI 互動」的簡化模型。

二、 專案實作:「西蒙說」遊戲

2.1 遊戲規則與 UI 設計

  • 四個彩色矩形(紅、藍、綠、黃)模擬按鈕,半透明疊加於 GameCanvas。
  • ControlCanvas 放置「雙手對準手型圖示啟動遊戲」的起始區域。
  • HandCanvas 顯示左右手游標(右手圖片 + ScaleTransform.ScaleX = -1 產生左手鏡像)。
  • 全螢幕固定解析度 1920×1080,外層 Viewbox 自動縮放適應螢幕。
<Canvas x:Name="GameCanvas">
  <Rectangle x:Name="RedBlock"   Fill="Red"    Canvas.Left="170"  Canvas.Top="90"   Opacity="0.2" />
  <Rectangle x:Name="BlueBlock"  Fill="Blue"   Canvas.Left="170"  Canvas.Top="550"  Opacity="0.2" />
  <Rectangle x:Name="GreenBlock" Fill="Green"  Canvas.Left="1350" Canvas.Top="550"  Opacity="0.2" />
  <Rectangle x:Name="YellowBlock" Fill="Yellow" Canvas.Left="1350" Canvas.Top="90"   Opacity="0.2" />
</Canvas>

遊戲 UI 設計圖

2.2 核心架構:狀態機與主循環

public enum GamePhase { GameOver = 0, SimonInstructing = 1, PlayerPerforming = 2 }
private GamePhase currentPhase;
private int currentLevel;
private UIElement[] instructionSequence;
private int instructionPosition;
private DispatcherTimer poseTimer; // 後續姿勢版擴充用

SkeletonFrameReady 事件根據 currentPhase 分派處理:

private void KinectDevice_SkeletonFrameReady(...)
{
    using (var frame = e.OpenSkeletonFrame())
    {
        if (frame == null) return;
        frame.CopySkeletonDataTo(frameSkeletons);
        var skeleton = GetPrimarySkeleton(frameSkeletons); // 最近的追蹤中骨架
        
        if (skeleton == null) { ChangePhase(GamePhase.GameOver); return; }

        if (currentPhase == GamePhase.SimonInstructing)
        {
            LeftHandElement.Visibility = RightHandElement.Visibility = Visibility.Collapsed;
        }
        else
        {
            TrackHand(skeleton.Joints[JointType.HandLeft],  LeftHandElement,  LayoutRoot);
            TrackHand(skeleton.Joints[JointType.HandRight], RightHandElement, LayoutRoot);

            switch (currentPhase)
            {
                case GamePhase.GameOver:        ProcessGameOver(skeleton);        break;
                case GamePhase.PlayerPerforming: ProcessPlayerPerforming(skeleton); break;
            }
        }
    }
}

2.3 遊戲啟動:雙手對準起始區

private void ProcessGameOver(Skeleton skeleton)
{
    if (HitTest(skeleton.Joints[JointType.HandLeft],  LeftHandStartElement) &&
        HitTest(skeleton.Joints[JointType.HandRight], RightHandStartElement))
    {
        ChangePhase(GamePhase.SimonInstructing);
    }
}

private bool HitTest(Joint joint, UIElement target)
    => GetHitTarget(joint, target) != null;

private IInputElement GetHitTarget(Joint joint, UIElement target)
{
    Point p = LayoutRoot.TranslatePoint(GetJointPoint(KinectDevice, joint, LayoutRoot.RenderSize, new Point()), target);
    return target.InputHitTest(p);
}

2.4 狀態切換與 UI 回饋

private void ChangePhase(GamePhase newPhase)
{
    if (newPhase == currentPhase) return;
    currentPhase = newPhase;

    switch (currentPhase)
    {
        case GamePhase.GameOver:
            currentLevel = 0;
            SetBlocksOpacity(0.2);
            GameStateElement.Text = "GAME OVER!";
            ControlCanvas.Visibility = Visibility.Visible;
            GameInstructionsElement.Text = "將手放在對象上開始新的遊戲。";
            break;

        case GamePhase.SimonInstructing:
            currentLevel++;
            GameStateElement.Text = $"Level {currentLevel}";
            ControlCanvas.Visibility = Visibility.Collapsed;
            GameInstructionsElement.Text = "注意觀察 Simon 的指示。";
            GenerateInstructions();
            DisplayInstructions(); // Storyboard 動畫依序亮起矩形
            break;

        case GamePhase.PlayerPerforming:
            instructionPosition = 0;
            GameInstructionsElement.Text = "請重復 Simon 的指示";
            break;
    }
}

2.5 指令生成與動畫顯示

private void GenerateInstructions()
{
    instructionSequence = new UIElement[currentLevel];
    for (int i = 0; i < currentLevel; i++)
    {
        switch (rnd.Next(1, 5)) // 1~4
        {
            case 1: instructionSequence[i] = RedBlock;    break;
            case 2: instructionSequence[i] = BlueBlock;   break;
            case 3: instructionSequence[i] = GreenBlock;  break;
            case 4: instructionSequence[i] = YellowBlock; break;
        }
    }
}

private void DisplayInstructions()
{
    var sb = new Storyboard();
    for (int i = 0; i < instructionSequence.Length; i++)
    {
        var anim = new DoubleAnimationUsingKeyFrames { FillBehavior = FillBehavior.Stop, BeginTime = TimeSpan.FromMilliseconds(i * 1500) };
        Storyboard.SetTarget(anim, instructionSequence[i]);
        Storyboard.SetTargetProperty(anim, new PropertyPath("Opacity"));
        anim.KeyFrames.Add(new EasingDoubleKeyFrame(0.3, KeyTime.FromTimeSpan(TimeSpan.Zero)));
        anim.KeyFrames.Add(new EasingDoubleKeyFrame(1.0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500))));
        anim.KeyFrames.Add(new EasingDoubleKeyFrame(1.0, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1000))));
        anim.KeyFrames.Add(new EasingDoubleKeyFrame(0.3, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1300))));
        sb.Children.Add(anim);
    }
    sb.Completed += (s, e) => ChangePhase(GamePhase.PlayerPerforming);
    sb.Begin(LayoutRoot);
}

2.6 玩家執行階段:命中測試與「手離開」偵測

初版邏輯:只要手進入目標矩形即判定成功。但玩家手若停留過久,下一幀仍在矩形內,會被誤判為連續點擊同一目標。解決方案:記錄上一幀命中元素,僅在「命中目標變化」且「上一目標為正確目標」時前進。

private IInputElement leftHandTarget, rightHandTarget;

private void ProcessPlayerPerforming(Skeleton skeleton)
{
    UIElement correctTarget = instructionSequence[instructionPosition];
    IInputElement leftTarget  = GetHitTarget(skeleton.Joints[JointType.HandLeft],  GameCanvas);
    IInputElement rightTarget = GetHitTarget(skeleton.Joints[JointType.HandRight], GameCanvas);

    bool targetChanged = (leftTarget != leftHandTarget) || (rightTarget != rightHandTarget);
    if (!targetChanged) return; // 手未移動,不處理

    // 雙手同時命中任何元素 → 犯規
    if (leftTarget != null && rightTarget != null) { ChangePhase(GamePhase.GameOver); return; }

    // 單手命中且為正確目標
    bool leftCorrect  = (leftTarget  == correctTarget && rightTarget == null);
    bool rightCorrect = (rightTarget == correctTarget && leftTarget  == null);
    if (leftCorrect || rightCorrect)
    {
        instructionPosition++;
        if (instructionPosition >= instructionSequence.Length)
            ChangePhase(GamePhase.SimonInstructing); // 過關
        // else 繼續等待下一指令
    }
    // 單手命中錯誤目標、或雙手皆未命中但手移動了 → 視為失誤
    else if (leftTarget != null || rightTarget != null)
    {
        ChangePhase(GamePhase.GameOver);
    }

    // 更新上一幀目標並給予視覺回饋(透明度變化)
    UpdateTargetVisual(leftTarget, ref leftHandTarget);
    UpdateTargetVisual(rightTarget, ref rightHandTarget);
}

private void UpdateTargetVisual(IInputElement newTarget, ref IInputElement oldTarget)
{
    if (newTarget == oldTarget) return;
    if (oldTarget is FrameworkElement oldFE) oldFE.Opacity = 0.2;
    if (newTarget is FrameworkElement newFE) newFE.Opacity = 1.0;
    oldTarget = newTarget;
}

遊戲執行畫面

三、 待改進方向與總結

3.1 使用者體驗

  • 啟動遊戲後給予 2~3 秒緩衝時間,避免雙手未移開即誤觸第一指令。
  • 關卡間加入倒數計時動畫,讓玩家準備。

3.2 遊戲內容

  • 指令生成策略多樣化:遞增長度、固定模式+隨機擴充、難度等級選擇。
  • 指令顯示速度隨關卡加快。

3.3 表現形式

  • 正確/錯誤時的粒子特效、音效獎懲。
  • 遊戲結束時回顧玩家動作截圖。

四、 結語

本文完整示範了如何在 WPF 中將 Kinect 骨骼資料轉化為類滑鼠/觸控輸入,涵蓋:

  1. WPF 輸入系統架構與 Kinect 整合缺口分析。
  2. 命中測試完整流程(座標轉換 → InputHitTest)。
  3. 「西蒙說」遊戲的狀態機設計、指令生成與動畫、玩家輸入判定與「手離開」偵測技巧。

下一篇將延續此專案,導入姿勢識別取代矩形碰撞測試,並討論可擴充的姿勢引擎架構,敬請期待!

作者:yangecnu(博客園)|原文連結:https://www.cnblogs.com/yangecnu/

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved