程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> Kinect for Windows SDK開發入門(八)骨骼追蹤進階 上

Kinect for Windows SDK開發入門(八)骨骼追蹤進階 上

編輯:關於.NET

前7篇文件我們介紹了Kinect SDK中各種傳感器的各種基本知識,我們用實驗的方式演示了這些基本對象和方法的如何使用,這些都是Kinect開發最基本的知識。了解了這些基本知識後,就可以開發出一個基於Kinect的簡單程序了。但是這些離開發出一個好的基於Kinect的應用程序還有一段距離。後面的文章中,將會結合Kinect SDK介紹WPF以及其它第三方工具,類庫來建立一個以Kinect為驅動的有較好用戶體驗的程序。我們將利用之前講到的知識來進行下面一些比較復雜的話題。

Kinect傳感器核心只是發射紅外線,並探測紅外光反射,從而可以計算出視場范圍內每一個像素的深度值。從深度數據中最先提取出來的是物體主體和形狀,以及每一個像素點的游戲者索引信息。然後用這些形狀信息來匹配人體的各個部分,最後計算匹配出來的各個關節在人體中的位置。這就是我們之前介紹過的骨骼追蹤。

紅外影像和深度數據對於Kinect系統來說很重要,它是Kinect的核心,在Kinect系統中其重要性僅次於骨骼追蹤。事實上,這些數據相當於一個輸入終端。隨著Kinect或者其他深度攝像機的流行和普及。開發者可以不用關注原始的深度影像數據,他們變得不重要或者只是作為獲取其他數據的一個基礎數據而已。我們現在就處在這個階段,Kinect SDK並沒有提供給開發者訪問原始紅外影像數據流的接口,但是其它第三方的SDK可以這麼做。可能大多數開發者不會使用原始的深度數據,用到的只是Kinect處理好了的骨骼數據。但是,一旦姿勢和手勢識別整合到Kinect SDK並成為其一部分時,可能開發者甚至不用接觸到骨骼數據了。

希望能夠早日實現這種集成,因為它代表這Kinect作為一種技術的走向成熟。本篇文章和下篇文章仍將討論骨骼追蹤,但是采用不同的方法來處理骨骼數據。我們將Kinect作為一個如同鼠標,鍵盤或者觸摸屏那樣的一個最基本的輸入設備。微軟當初推出Kinect for Xbox的口號是“你就是控制器”,從技術方面講,就是“你就是輸入設備”。通過骨骼數據,應用程序可以做鼠標或者觸摸屏可以做的事情,所不同的是深度影像數據使得用戶和應用程序可以實現以前從沒有過的交互方法。下面來看看Kinect控制並與用戶界面進行交互的機制吧。

1. 用戶交互

運行在電腦上的應用程序需要輸入信息。傳統的信息來自於鼠標或者鍵盤等這些輸入設備。用戶直接與這些硬件設備進行交互,然後硬件設備響應用戶的操作,將這些操作轉換成數據傳輸到計算機中。計算機接收這些輸入設備的信息然後將結果以可視化的形式展現出來。大多數計算機的圖像用戶界面上會有一個光標(Cursor),他通常代表鼠標所在的位置,因為鼠標是最開始有個滾輪設備。但是現在,如果將這個光標指代鼠標光標的話,可能不太准確,因為現在一些觸摸板或手寫設備也能像鼠標那樣控制光標。當用戶移動鼠標或者在觸摸板上移動手指時,光標也能響應這種變化。當用戶將光標移動到一個按鈕上時,通常按鈕的外觀會發生變化,提示用戶光標正位於按鈕上。當用戶點擊按鈕時,按鈕則為顯示另一種外觀。當用戶松開鼠標上的按鍵,按鈕就會出現另外一種外觀。顯然,簡單的點擊事件會涉及到按鈕的不同狀態。

開發者可能對這些交互界面和操作習以為常,因為諸如WPF之類的用戶交互平台使得程序與用戶進行交互變得非常簡單。當開發網頁程序時,浏覽器響應用戶的交互,開發者只需要根據用戶鼠標的懸停狀態來設置樣式即可進行交互。但是Kinect不同,他作為一個輸入設備,並沒有整合到WPF中去,因此,作為一個開發者。對操作系統和WPF所不能直接響應的那部分工作需要我們來完成。

在底層,鼠標,觸摸板或者手寫設備都是提供一些X,Y坐標,操作系統將這些X,Y坐標從其在的空間坐標系統轉換到計算機屏幕上,這一點和上篇文章討論的空間變換類似。操作系統的職責是響應這些標准輸入設備輸入的數據,然後將其轉換到圖形用戶界面或者應用程序中去。操作系統的圖形用戶界面顯示光標位置,並響應用戶的輸入。在有些時候,這個過程沒有那麼簡單,需要我們了解GUI平台。以WPF應用程序為例,它並沒有對Kinect提供像鼠標和鍵盤那樣的原生的支持。這個工作就落到開發者身上了,我們需要從Kinect中獲取數據,然後利用這些數據與按鈕,下拉框或者其他控件進行交互。根據應用程序或者用戶界面的復雜度的不同,這種工作可能需要我們了解很多有關WPF的知識。

1.1 WPF應用程序中輸入系統介紹

當開發一個WPF應用程序時,開發者並不需要特別關注用戶輸入機制。WPF會為我們處理這些機制使得我們可以關注於如何響應用戶的輸入。畢竟作為一個開發者,我們更應該關心如何對用戶輸入的信息進行分析處理,而不是重新造輪子來考慮如何去收集用戶的輸入。如果應用程序需要一個按鈕,只需要從工具箱中拖一個按鈕出來放在界面上,然後在按鈕的點擊事件中編寫處理邏輯即可。在大多數情況下,開發者可能需要對按鈕設置不同的外觀以響應用戶鼠標的不同狀態。WPF會在底層上為我們實現這些事件,諸如鼠標何時懸停在按鈕上,或者被點擊。

WPF有一個健全的輸入系統來從輸入設備中獲取用戶的輸入信息,並響應這些輸入信息所帶來的控件變化。這些API位於System.Windows.Input命名空間中(Presentation.Core.dll),這些API直接從操作系統獲取輸入設備輸入的數據,例如,名為Keyboard,Mouse,Stylus,Touch和Cursor的這些類。InputManager這個類負責管理所有輸入設備獲取的信息,並將這些信息傳遞到表現框架中。

WPF的另一類組件是位於System.Windows命名空間(PresentationCore.dll)下面的四個類,他們是UIElement,ContentElement,FrameworkElement以及FrameworkContentElement 。FrameworkElement繼承自UIElement,FrameworkContentElement繼承自ContentElement。這幾個類是WPF中所有可視化元素的基類,如Button,TextBlock及ListBox。更多WPF輸入系統相關信息可以參考MSDN文檔。

InputManager監聽所有的輸入設備,並通過一系列方法和事件來通知UIElement和ContentElement對象,告知這些對象輸入設備進行了一些有關可視化元素相關的操作。例如,在WPF中,當鼠標光標進入到可視化控件的有效區域時就會觸發MouseEnterEvent事件。UIElement和ContentElement對象也有OnMouseEnter事件。這使得任何繼承自UIElement或者ContentElement類的對象也能夠接受來自輸入設備的所觸發的事件。WPF會在觸發任何其它輸入事件之前調用這些方法。在UIElement和ContentElement類中也有一些類似的事件包括MouseEnter,MouseLeave,MouseLeftButtonDown,MouseLeftButtonUp,TouchEnter,TouchLeave,TouchUp和TouchDown。

有時候開發者需要直接訪問鼠標或者其他輸出設備,InputManager對象有一個稱之為PrimaryMouseDevice的屬性。他返回一個MouseDevice對象。使用MouseDevice對象,能夠在任何時候通過調用GetScreenPositon來獲取鼠標的位置。另外,MouseDevice有一個名為GetPositon的方法,可以傳入一個UI界面元素,將會返回在該UI元素所在的坐標空間中的鼠標位置。當需要判斷鼠標懸停等操作時,這些信息尤其重要。當Kinect SDK每一次產生一幅新的SkeletonFrame幀數據時,我們需要進行坐標空間轉換,將關節點位置信息轉換到UI空間中去,使得可視化元素能夠直接使用這些數據。當開發者需要將鼠標作為輸入設備時, MouseDevice對象中的GetScreenPositon和GetPosition方法能提供當前鼠標所在點的位置信息。

在有些情況下,Kinect雖然和鼠標相似,但是某些方面差別很大。骨骼節點進入或者離開UI上的可視化元素這一點和鼠標移入移出行為類似。換句話說,關節點的懸停行為和鼠標光標一樣。但是,類似鼠標點擊和鼠標按鈕的按下和彈起這些交互,關節點與UI的交互是沒有。在後面的文章中,可以看到使用手可以模擬點擊操作。在Kinect中相對於實現鼠標移入和移出操作來說,對鼠標點擊這種支持相對來說較弱。

Kinect和觸摸板也沒有太多相同的地方。觸摸輸入可以通過名為Touch或者TouchDevice的類來訪問。單點的觸摸輸入和鼠標輸入類似,然而,多點觸控是和Kinect類似的。鼠標和UI之間只有一個交互點(光標)但是觸摸設備可以有多個觸控點。就像Kinect可以有多個游戲者一樣。從每一個游戲者身上可以捕捉到20個關節點輸入信息。Kinect能夠提供的信息更多,因為我們知道每一個輸入點是屬於游戲者身體的那個部位。而觸控輸入設備,應用程序不知道有多少個用戶正在觸摸屏幕。如果一個程序接收到了10個輸入點,無法判斷這10個點是一個人的10個手指還是10個人的一個手指觸發的。 雖然觸控設備支持多點觸控,但這仍然是一種類似鼠標或者手寫板的二維的輸入。然而,觸控輸入設備除了有X,Y點坐標外,還有觸控接觸面積這個字段。畢竟,用戶用手指按在屏幕上沒有鼠標光標那樣精確,觸控接觸面積通常大於1個像素。

當然,他們之間也有相似點。Kinect輸入顯然嚴格地符合WPF 所支持的任何輸入設備的要求。除了有其它輸入設備類似的輸入方式外,他有獨特的和用戶進行交互的方式和圖形用戶界面。核心上,鼠標,觸控板和手寫板只傳遞一個像素點位置嘻嘻你。輸入系統確定該點在可見元素上下文中的像素點位置,然後這些相關元素響應這個位置信息,然後進行響應操作。

期望是在未來Kinect能夠完整的整合進WPF。在WPF4.0中,觸控設備作為一個單獨的模塊。最開始觸控設備被作為微軟的Surface引入。Surface SDK包括一系列的WPF控件,諸如SurfaceButton,SurfaceCheckBox,和SurfaceListBox。如果你想按鈕能夠響應觸摸事件,最好使用SurfaceButton控件。

能夠想象到,如果Kinect被完整的整合進WPF,可能會有一個稱之為SkeletonDevice的類。他和Kinect SDK中的SkeletonFrame對象類似。每一個Skeleton對象會有一個稱之為GetJointPoint的方法,他和MouseDevice的GetPositon和TouchDevice的GetTouchPoint類似。另外,核心的可視化元素(UElement, ContentElement, FrameworkElement, FrameworkContentElement) 有能夠相應的事件或者方法能夠通知並處理骨骼關節點交互。例如,可能有一個JointEnter,JointLeave,和JointHover事件。更進一步,就像觸控類有一個ManipulationStarted和ManipulationEnded事件一樣,在Kinect輸入的時候可能伴隨GetstureStarted和GestureEnded事件。

目前,Kinect SDK和WPF是完全分開的,因此他和輸入系統沒有在底層進行整合。所以作為開發者的我們需要追蹤骨骼關節點位置,並判斷節點位置是否和UI界面上的元素有交互。當關節點在對應的UI坐標系可視化界面的有效范圍內時,我們必須手動的改變這些可視化元素的外觀以響應這種交互。

1.2 探測用戶的交互

在確定用戶是否和屏幕上的某一可視化元素進行交互之前,我們必須定義什麼叫用戶和可視化元素的交互。在以鼠標或者光標驅動的應用程序中有兩種用戶交互方式。鼠標懸停和點擊交互。這些將事件劃分為更精細的交互。就拿光標懸停來說,它必須進行可視化組件的坐標空間區域,當光標離開這一區域,懸停交互也就結束了。在WPF中,當用戶進行這些操作時,會觸發MouseEnter和MouseLeave操作。

除了點擊和懸停外,鼠標還有另外一種常用的交互,那就是拖放。當光標移動到可視化組件上方,按下鼠標左鍵,然後在屏幕上拖動,我們稱之為拖動(drag),當用戶松開鼠標左鍵時,我們之位釋放操作(drop)。鼠標拖動和釋放是一個比較復雜的交互,這點和Kinect中的手勢類似。

本節我們來看一下一些簡單的諸如光標懸停,進入,離開可視化控件的交互。在前篇文章中的Kinect連線小游戲中,我們在繪制直線時需要判斷手是否在點的合適范圍內。在那個小游戲中,應用程序並沒有像用戶界面和人那樣直接響應用戶界的操作。這種差別很重要。應用程序在屏幕坐標空間中產生一些點的位置(數字),但是這些點並沒有直接從屏幕空間派生。這些點只是存儲在變量中的數據而已。我們改變屏幕大小使得很容易展現出來。在接收到新的骨骼數據幀之前。骨骼數據中手的位置被轉換到屏幕中點所在的空間坐標系,然後我們判斷手所在的位置的點是否在點序列中。技術上來講,這個應用程序即使沒有用戶界面也能夠正常運行。用戶界面是動態的由這些數據產生的。用戶直接和這些數據而不是和界面進行交互。

1.2.1命中測試(Hit testing)

判斷用戶的手是否在點的附近遠沒有判斷手是否在點的位置上那麼簡單。每一個點只是一個象元。為了使得應用程序能夠工作。我們並不要求手的位置敲好在這個點所在的象元上,而是要求在以這個點為中心的某一個區域范圍內。我們在點的周圍創建了一個圓圈代表點的區域范圍,用戶的手的中心必須進入到這個點的區域范圍才被認為是懸停在該點上。如圖所示在圓形中的白色的點是實際的點,虛線繪制的圓形是該點的最大可觸及范圍。手形圖標的中心用白色的點表示。所以,有可能手的圖標和點的最大范圍接觸了,但是手的中心卻不在該點的最大范圍內。判斷手的中心是否在點的最大范圍之內稱之為命中測試。

在Kinect連線游戲中,用戶界面響應數據,依據產生的坐標將點繪制在圖像界面上,系統使用點而不是用可視化控件的有效區間來進行命中測試。大多數的應用程序和游戲都不是這樣做的。用戶界面通常很復雜,而且是動態的。例如在Kinect for Windows SDK中自帶的ShapeGame應用就是這樣一個例子,它動態的從上至下產生一些形狀。當用戶觸碰這些形狀時形狀會消失或者彈開。

ShapeGame這個應用比之前的Kinect連線游戲需要更為復雜的命中測試算法。WPF提供了一些工具來幫助我們實現命中測試。在System.Windows.Media命名空間下的VisualTreeHelper幫助類中有一個HitTest方法。這個方法有很多個重載,但是最基本的方法接受兩個參數,一個是可視化控件對象,另一個是待測試的點。他返回可視化對象樹中該點所命中的最頂層的那個可視化對象。聽起來可能有點復雜,一個最簡單的解釋是,在WPF中有一個分層的可視化輸出,有多個對象可能占據同一個相對空間,但是在不同的層。如果該點所在位置有多個對象,那麼HitTest返回處在可視化樹中處在最頂層的可視化對象。由於WPF的樣式和模板系統使得一個控件能夠由一個或者多個元素或者其它控件組成,所在通常在一個點可能有多個可視化元素。

上圖可能幫助我們理解可視元素的分層。圖中有三個元素:圓形,矩形和按鈕。所有三個元素都在Canvas容器中。圓形和按鈕在矩形之上,左邊第一幅圖中,鼠標位於圓形之上,在這點上的命中測試結果將返回這個圓形。第二幅圖,即使矩形最底層,由於鼠標位於矩形上,所以命中測試會返回矩形。這是因為矩形在最底層,他是唯一個占據了鼠標光標象元所在位置的可視化元素。在第三幅圖中,光標在按鈕的文字上,命中測試將返回TextBlock對象,如果鼠標沒有位於按鈕的文字上,命中測試將會返回ButtonChrome元素。按鈕的可視化表現通常由一個或者多個可視化控件組成,並能夠定制。實際上,按鈕沒有繼承可視化樣式,它是一個沒有可視化表現的控件。上圖中的按鈕使用的是默認樣式,它由TextBlock和ButtonChrome這兩個控件構成的。在這個例子中,我們通常會獲得到有按鈕樣式組成的元素,但是永遠獲取不到實際的按鈕控件。

為了使得命中測試更為方便,WPF提供了其他的方法來協助進行命中測試。UIElement類定義了一個InputHitTest方法,它接受一個Point對象,並返回該Point對象指定的一個IIputElement元素。UIElement和ContentElement兩個類都實現了IInputElement接口。這意味著所有的WPF用戶界面元素都實現了這個接口。VisualTreeHelper類中的HitTest方法可以用在一般的場合。

Note: MSDN中關於UIElement.InputHitTest方法的建議“應用程序一般不需要調用該方法,只有應用程序需要自己重新實現一系列已經實現了的底層輸入特征,例如要重新實現鼠標設備的輸入邏輯時才會去調用該方法。”由於Kinect並沒有原生的集成到WPF中,所以必須重新實現類似鼠標設備的輸入邏輯。

WPF中,命中測試依賴於兩個變量,一個是可視化元素,另一個是點。測試首先該點轉換到可視化元素所在坐標空間,然後確定是否處於該可視化元素的有效范圍內。下圖可以更好的理解可視化元素的坐標空間。WPF中的每一個可視化元素,不論其形狀和大小,都有一個外輪廓:這個輪廓是一個矩形,它包含可視化元素並定義了可視化元素的寬度和高度。布局系統使用這個外輪廓來確定可視化元素的整體尺寸以及如何將其排列在屏幕上。當開發者使用Canvas,Grid,StackPanel等容器來布局其子元素時,元素的外輪廓是這些容器控件如進行布局計算的基礎。用戶看不到元素的外輪廓,下圖中,可視化元素周圍的虛線矩形顯示了這些元素的外輪廓。此外,每一個元素有一個X,Y坐標用來指定該元素在其父容器中的位置。可以通過System.Windows.Controls.Primitives命名空間中的LayoutInformation靜態類中的GetLayoutSlot方法來獲取元素的外輪廓和其位置。舉例來說,圖中三角形的外輪廓的左上角坐標點為(0,0),三角形的寬和高都是200像素。所以在三角形外輪廓中,三角形的三個點的坐標分別為(100,0),(200,200),(0,200)。並不是在三角形外輪廓中的所有點在命中測試中都會成功,只有在三角形內部的點才會成功。點(0,0)不會命中,而三角形的中心(100,100)則能命中。

命中測試的結果依賴於可視化元素的布局。在目前所有的項目中,我們使用Canvas容器來包含所有可視化元素。Canvas是一個可視化的容器,能夠使得開發者對可視化元素的位置進行完全控制,這一點在使用Kinect的時候尤其明顯。像手部跟蹤這類基本的方法也可以使用WPF中的其他容器,但是需要做很多其他工作,並且性能沒有使用Canvas好。使用Cnavas容器,用戶可以通過CanvasLeft和CanvasTop顯式設定其所有子元素的起始X,Y的位置。前面討論的坐標空間轉換使用Cnavas作為容器,因為不需要太多的處理操作,轉換也非常明了,只需要少量的代碼就可以實現較好的性能。

使用Canvas作為容器的缺點也是其的優點。由於開發者可以完全控制在Canvas中子元素的位置,所以當窗體大小發生改變或者有比較復雜的布局時,也需要開發者去更新這些可視化元素的位置。而另外一些容器控件,如Grid,StackPanel則會幫助我們實現這些更新操作。但是,這些容器控件增加了可視化樹的結構和坐標空間,從而增加了命中測試的復雜度。坐標空間越多,需要的點的轉換就越多。這些容器還有alignment屬性(水平和垂直)和相對於FrameworkElement的margin屬性,進一步增加了命中測試的計算復雜度。如果可是化元素有RenderTransforms方法的話,我們可以直接使用這些方法而不用去自己寫命中測試的算法了。

一個折中的方法是,將那些基於骨骼節點位置的需要頻繁變化的可視化元素,如手形圖標放在Canvas容器內,而將其他UI元素放在其他容器控件內。這種布局模式需要多個坐標空間轉換,會影響程序性能,並且在進行坐標空間轉換計算時可能會引入一些bug。這種混合的布局方案在大多數情況下是最好的選擇,它充分利用了WPF布局系統的優點。要詳細了解各種容器及其命中測試的相關概念,可以參閱MSDN中WPF的布局系統。

1.2.2響應輸入

命中測試只能告訴當前用戶輸入點是否在可視化元素的有效區間內。用戶界面最重要的一個功能是要給予用戶一些對輸入操作的反饋。當鼠標移到一個按鈕上時,我們期望按鈕能夠改變其外觀,告訴我們這個按鈕是可以點擊的。如果沒有這種反饋,用戶不僅用戶體驗不好,而且還會使用戶感到迷惑。有時候即使功能完備,用戶體驗失敗意味著應用的失敗。

WPF有一套功能強大的系統來通知和響應用戶的輸入。只要用戶的輸入設備是鼠標,手寫筆,觸摸板這些標准設備,WPF的樣式和模版系統使得開發出能夠響應用戶輸入的高度的定制化的用戶界面非常容易。而Kinect的開發者有兩種選擇:不使用WPF系統提供的功能,手動實現所有功能,或者創建一個通用的控件來響應Kinect輸入。第二種方法雖然不是特別難,但是初學者也不太容易能夠實現。

了解了這一點,在下面的章節中,我們將會開發一個游戲來使用命中測試並手動響應用戶的輸入。在開始之前,思考一個問題,到目前位置,還有那些問題沒有很好解決?使用Kinect骨骼數據和用戶界面進行交互是什麼意思?核心的鼠標交互有:進入,離開和點擊。觸摸輸入交互有進入,離開,按下,彈起。鼠標只有一個觸控點,觸摸版可以有多個觸控點,但是只有一個是主觸控點。Kinect骨骼節點數據有20個可能的數據點,哪一個點是主觸控點?應該有一個主控點嗎?一個可視化元素,比如說按鈕,會在任何一個關節點數據到達按鈕的有效范圍內觸發,還是只是特定的關節點數,比如手,進入范圍後才能觸發?

沒有一個回答能夠完全回答好上面的問題。這取決於應用程序界面的設計及要實現的功能。這些問題其實是自然交互界面設計中的一部分典型問題。在後面我們會介紹。對於大多數Kinect應用程序,包括本文中的例子,只允許手部關節點數據才能和用戶界面進行交互。最開始的交互是進入和離開。除此之外的交互可能會很復雜。在後面我們將介紹這些復雜的交互,現在讓我們來看看最基本的交互。

2.  “我說你做”游戲

為了演示如何將Kinect作為一個輸入設備,我們開始開發我們的項目:該項目使用手部關節點數據模仿鼠標或者觸控板和用戶界面進行交互。這個項目的目標是展示如何進行命中測試和使用WPF可視化元素來創建用戶界面。項目是一個稱之為“我說你做”(Simon Say)的小游戲。

“我說你做”(Simon says)是一個英國傳統的兒童游戲。一般由3個或更多的人參加。其中一個人充當"Simon"。其他人必須根據情況對充當"Simon"的人宣布的命令做出不同反應。如果充當"Simon"的人以"Simon says"開頭來宣布命令,則其他人必須按照命令做出相應動作。如:充當"Simon"的人說:"Simon says jump(跳)"。其他人就必須馬上跳起;而如果充當"Simon"的人沒有說"Simon says"而直接宣布命令,如:充當"Simon"的人說"jump"。則其他人不准有動作,如果有動作則做動作的人被淘汰出游戲。

在70年代末80年代初有一個叫Milton Bradley的游戲公司開發了一個電子版的Simon say游戲。該游戲界面由4個不同顏色 (紅色,藍色,綠色,黃色) 的按鈕組成,這個游戲在電腦上運行,讓游戲者按演示的順序按下這些按鈕。當開始游戲時,程序首先按照一定的順序亮起每一個按鈕,游戲者必須按照這個亮燈的順序依次按下這些按鈕。如果游戲者操作正確,那麼下一個亮燈序列又開始,到後面變化會越來越快,直到游戲者不能夠按照給定的順序按下這些按鈕位置。

我們要做的是,使用Kinect設備來實現這麼一個Simon Say游戲。這是個很好的使用Kinect展示如何和用戶界面進行交互的例子。這個游戲也有一些規則。下圖展示了我們將要做的用戶界面,他包含四個矩形,他用來模擬游戲中的按鈕。界面上方是游戲標題,中間是游戲的操作指南。

這個Kinect版的Simon says游戲追蹤游戲者的手部關節。當用戶的手碰到了這四個填充了顏色的方框中的任何一個時,程序認為游戲者按下了一個按鈕。在Kinect應用程序中,使用懸停或者點擊來和按鈕進行交互很常見。現在,我們的游戲操作指南還很簡單。游戲一開始,我們提示用戶將手放在界面上紅色矩形中手勢圖標所在的位置。在用戶將雙手放到指定位置後,界面開始發出指令。如果游戲者不能夠重復這個過程,游戲將會結束,並返回到這個狀態。現在,我們對這個游戲的概念,規則和樣子有了一些了解,現在開始編碼。

2.1 Simon say “設計一個用戶界面”

首先來設計一個用戶界面,下面的代碼展示的主界面中的XAML和之前的連線游戲一樣,我們將所有的主界面的UI元素包含在Viewbox容器中,讓他來幫助我們進行不同顯示器分辨率下面的縮放操作。主UI界面分辨率設置為1920*1080。UI界面共分為4個部分:標題及游戲指導,游戲界面,游戲開始界面以及用來追蹤手部的手形圖標。第一個TextBlock用來顯示標題,游戲引導放在接下來的StackPanel元素中。這些元素是用來給游戲者提供當前游戲狀態。他們沒有功能性的作用,和Kinect或者骨骼追蹤沒有關系。

GameCanvas,ControlCanvas和HandCanvas包含了所有的和Kienct相關的UI元素,這些元素是基於當前用戶手的位置和用戶界面進行交互的。手的位置來自骨骼追蹤。HandCanvas應該比較熟悉,程序中有兩個手形圖標,用來追蹤游戲者兩只手的運動。ControlCanvas存儲的UI元素用來觸發開始游戲。GameCanvas用來存儲這4個矩形,在游戲中,用戶需要點擊這些矩形。不同的交互元素存儲在不同的容器中,使得用戶界面能夠比較容易使用代碼進行控制。比如,當用戶開始游戲後,我們需要隱藏所有的ControlCanvas容器內的子元素,顯然隱藏這個容器比隱藏其每個子控件容易的多。整個UI代碼如下:

<Window x:Class="KinectSimonSay.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:KinectSimonSay"  
        Title="MainWindow" WindowState="Maximized">
    <Viewbox>
        <Grid x:Name="LayoutRoot" Height="1080" Width="1920" Background="White" TextElement.Foreground="Black">
            <c:SkeletonViewer x:Name="SkeletonViewerElement"/>
            <TextBlock Text="Simon Say" FontSize="72" Margin="0,25,0,0" HorizontalAlignment="Center" VerticalAlignment="Top"></TextBlock>
            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Width="600">
                <TextBlock x:Name="GameStateElement" FontSize="55" Text=" GAME OVER!" HorizontalAlignment="Center" />
                <TextBlock x:Name="GameInstructionsElement" Text="將手放在對象上開始游戲。" FontSize="45" HorizontalAlignment="Center"
                           TextAlignment="Center"  TextWrapping="Wrap" Margin="0,20,0,0" />
            </StackPanel>
                
            <Canvas x:Name="GameCanvas">
                <Rectangle x:Name="RedBlock" Height="400" Width="400" Fill="Red" Canvas.Left="170" Canvas.Top="90" Opacity="0.2" />
                <Rectangle x:Name="BlueBlock" Height="400" Width="400" Fill="Blue" Canvas.Left="170" Canvas.Top="550" Opacity="0.2" />
                <Rectangle x:Name="GreenBlock" Height="400" Width="400" Fill="Green" Canvas.Left="1350" Canvas.Top="550" Opacity="0.2" />
                <Rectangle x:Name="YellowBlock" Height="400" Width="400" Fill="Yellow" Canvas.Left="1350" Canvas.Top="90" Opacity="0.2" />
            </Canvas>
                
            <Canvas x:Name="ControlCanvas">
                <Border x:Name="RightHandStartElement" Background="Red" Height="200" Padding="20" Canvas.Left="1420" Canvas.Top="440" >
                    <Image Source="Images/hand.png" />
                </Border>
                <Border x:Name="LeftHandStartElement" Background="Red" Height="200" Padding="20" Canvas.Left="300" Canvas.Top="440" >
                    <Image Source="Images/hand.png" >
                        <Image.RenderTransform>
                            <TransformGroup>
                                <TranslateTransform X="-130" />
                                <ScaleTransform ScaleX="-1" />
                            </TransformGroup>
                        </Image.RenderTransform>
                    </Image>
                        
                </Border>
            </Canvas>
                
            <Canvas x:Name="HandCanvas">
                <Image x:Name="RightHandElement" Source="Images/hand.png" Visibility="Collapsed" Height="100" Width="100" />
                <Image x:Name="LeftHandElement" Source="Images/hand.png" Visibility="Collapsed" Height="100" Width="100" >
                    <Image.RenderTransform>
                        <TransformGroup>
                            <ScaleTransform ScaleX="-1" />
                            <TranslateTransform X="90" />
                        </TransformGroup>
                    </Image.RenderTransform>
                </Image>
            </Canvas>
        </Grid>
    </Viewbox>
</Window>

2.2 Simon say “構建程序的基礎結構”

UI界面設計好了之後,我們現在來看游戲的基礎結構。需要在代碼中添加響應SkeletonFrameReady事件的邏輯。在SkeletonFrameReady事件中,添加代碼來跟蹤游戲者手部關節的運動。基本代碼如下:

private void KinectDevice_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    using (SkeletonFrame frame = e.OpenSkeletonFrame())
    {
        if (frame != null)
        {
            frame.CopySkeletonDataTo(this.frameSkeletons);
            Skeleton skeleton = GetPrimarySkeleton(this.frameSkeletons);
    
            if (skeleton == null)
            {
                ChangePhase(GamePhase.GameOver);
            }
            else
            {
                LeftHandElement.Visibility = Visibility.Collapsed;
                RightHandElement.Visibility = Visibility.Collapsed;
            }
        }
    }
}
    
private static Skeleton GetPrimarySkeleton(Skeleton[] skeletons)
{
    Skeleton skeleton = null;
    
    if (skeletons != null)
    {
        //Find the closest skeleton       
        for (int i = 0; i < skeletons.Length; i++)
        {
            if (skeletons[i].TrackingState == SkeletonTrackingState.Tracked)
            {
                if (skeleton == null)
                {
                    skeleton = skeletons[i];
                }
                else
                {
                    if (skeleton.Position.Z > skeletons[i].Position.Z)
                    {
                        skeleton = skeletons[i];
                    }
                }
            }
        }
    }
    
    return skeleton;
}

上面代碼中TrackHand和GetJointPoint代碼和Kinect連線游戲中相同。對於大多數游戲來說,使用“拉模型”來獲取數據比使用事件模型獲取數據性能要好。游戲通常是一個循環,可以手動的從骨骼數據流中獲取下一幀骨骼數據。但是在我們的例子中,仍然使用的是事件模型,為的是能夠減少代碼量和復雜度。

2.3 Simon say “添加游戲基本元素”

Simon say游戲分成三步。起始步驟,我們之為GameOver,意味著當前沒有可以玩的游戲。這是游戲的默認狀態。這也是當Kinect探測不到游戲者時所切換到的狀態。然後游戲開始循環,Simon給出一些指令,然後游戲者重復執行這些指令,重復這一過程,直到用戶沒能夠正確的執行Simon給出的指令為止。應用程序定義了一個枚舉變量來描述游戲所有可能的狀態,以及定義了一個變量來跟蹤游戲這當前所執行了的指令位置。另外我們需要一個變量來描述游戲者成功的次數或者游戲等級。當游戲者成功的執行了Simon給出的指令後,這個變量加1。下面的代碼展示了這個枚舉以及變量,變量的初始化在類的夠著函數中執行。

public enum GamePhase
{
  GameOver = 0,
  SimonInstructing = 1,
  PlayerPerforming = 2
}
    
public MainWindow()
{
    InitializeComponent();
    KinectSensor.KinectSensors.StatusChanged += KinectSensors_StatusChanged;
    this.KinectDevice = KinectSensor.KinectSensors.FirstOrDefault(x => x.Status == KinectStatus.Connected);
    ChangePhase(GamePhase.GameOver);
    this.currentLevel = 0;
}

SkeletonFrameReady事件需要根據當前游戲所處的狀態來執行不同的操作。下面的代碼中根據當前游戲的狀態執行ChangePhase,ProcessGameOver和ProcessPlayerPerforming子方法。這些方法的詳細執行過程將在後面介紹。ChangePhase方法接受一個GamePhase枚舉值,後兩個方法接受一個Skeleton類型的參數。

當應用程序探測不到骨骼數據時,游戲會終止,並切換到Game Over階段。當游戲者離開Kinect視野時會發生這種情況。當游戲處在Simon給出操作步驟階段時,隱藏界面上的手勢圖標。否則,更新這兩個圖標的位置。當游戲處在其它狀態時,程序基於當前特定的游戲階段調用特定的處理方法。

private void KinectDevice_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
{
    using (SkeletonFrame frame = e.OpenSkeletonFrame())
    {
        if (frame != null)
        {
            frame.CopySkeletonDataTo(this.frameSkeletons);
            Skeleton skeleton = GetPrimarySkeleton(this.frameSkeletons);
    
            if (skeleton == null)
            {
                ChangePhase(GamePhase.GameOver);
            }
            else
            {
                if (this.currentPhase == GamePhase.SimonInstructing)
                {
                    LeftHandElement.Visibility = Visibility.Collapsed;
                    RightHandElement.Visibility = Visibility.Collapsed;
                }
                else
                {
                    TrackHand(skeleton.Joints[JointType.HandLeft], LeftHandElement, LayoutRoot);
                    TrackHand(skeleton.Joints[JointType.HandRight], RightHandElement, LayoutRoot);
    

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

2.4 開始新游戲

當游戲處在GameOver階段時,應用程序只調用了一個方法:該方法判斷用戶是否想玩游戲。當用戶將相應的手放在UI界面上手勢所處的位置時,游戲開始。左右手需要分別放在LeftHandStartElement和RightHandStartElement所處的位置內。在這個例子中,我們使用WPF自帶的命中測試功能。我們的UI界面很小也很簡單。InputHitTest操作所需要處理的UI元素很少,因此性能上沒有太大問題。下面的代碼展示了ProcessGameOver方法和GetHitTarget方法。

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)
{
    return (GetHitTarget(joint, target) != null);
}
    
private IInputElement GetHitTarget(Joint joint, UIElement target)
{
    Point targetPoint = LayoutRoot.TranslatePoint(GetJointPoint(this.KinectDevice, joint, LayoutRoot.RenderSize, new Point()), target);
    return target.InputHitTest(targetPoint);
}

ProcessGameOver方法的邏輯簡單明了:如果游戲者的任何一只手在UI界面上的對應位置,就切換當前游戲所處的狀態。GetHitTarget方法用來測試給定的關節點是否在可視化控件有效范圍內。他接受關節點數據和可視化控件,返回該點所在的特定的IInputElement對象。雖然代碼只有兩行,但是了解背後的邏輯很重要。

命中測試算法包含三個步驟,首先需要將關節點所在的骨骼空間坐標系中坐標轉換到對應的LayoutRoot元素所在的空間坐標中來。GetJointPoint實現了這個功能。其次,使用UIElement類中的TranslatePoint方法將關節點從LayoutRoot元素所在的空間坐標轉換到目標元素所在的空間坐標中。最後,點和目標元素在一個坐標空間之後,調用目標元素的InputHitTest方法,方法返回目標對象樹中,點所在的確切的UI元素,任何非空值都表示命中測試成功。

注意到邏輯之所以這麼簡單是因為我們采用的UI布局方式,應用程序假定全屏運行並且不能調整大小。將UI界面設置為靜態的,確定大小能夠極大的簡化計算量。另外,將所有的可交互的UI元素放在Canvas容器內使得我們只有一個坐標空間。使用其他容器空間來包含元素或者使用諸如HorizonAlignment,VerticalAlignment或者Margin這些自動布局屬性會增加命中測試的復雜性。簡言之,越是復雜的UI布局,命中測試的邏輯越復雜,也越會影響程序的性能。

2.4.1 更改游戲狀態

編譯並運行程序,如果沒問題的話,結果應該如下圖。應用程序能夠追蹤手部的運動,並且當用戶將手放到對應的位置後,應用程序的狀態會從GameOver轉到SimonInstructing狀態。下一步是要實現ChangePhase方法,代碼如下:

private void ChangePhase(GamePhase newPhase)
{
    if (newPhase != this.currentPhase)
    {
        this.currentPhase = newPhase;
    
        switch (this.currentPhase)
        {
            case GamePhase.GameOver:
                this.currentLevel = 0;
                RedBlock.Opacity = 0.2;
                BlueBlock.Opacity = 0.2;
                GreenBlock.Opacity = 0.2;
                YellowBlock.Opacity = 0.2;
    
                GameStateElement.Text = "GAME OVER!";
                ControlCanvas.Visibility = Visibility.Visible;
                GameInstructionsElement.Text = "將手放在對象上開始新的游戲。";
                break;
    
            case GamePhase.SimonInstructing:
                this.currentLevel++;
                GameStateElement.Text = string.Format("Level {0}", this.currentLevel);
                ControlCanvas.Visibility = Visibility.Collapsed;
                GameInstructionsElement.Text = "注意觀察Simon的指示。";
                GenerateInstructions();
                DisplayInstructions();
                break;
    
            case GamePhase.PlayerPerforming:
                this.instructionPosition = 0;
                GameInstructionsElement.Text = "請重復 Simon的指示";
                break;
        }
    }
}

上面的代碼和Kinect無關,事實上可以使用鼠標或者觸控板來實現這一步,但是這段代碼是必須的。ChangePhase方法用來控制UI界面來顯示當前游戲狀態的變化,維護一些游戲進行所需要的數據。在GameOver狀態時,矩形框會漸變消失,然後改變操作指示,顯示按鈕來開始一個新的游戲。SimonInStructing狀態不在更新UI界面討論范圍內,他調用了兩個方法,用來產生指令集合 (GenerateInstructions),並將這些指令顯示到UI界面上(DisplayInstructions),代碼中也定義了instructionPosition變量,來維護當前所完成的指令步驟。

2.4.2 顯示Simon的指令

下面的代碼顯示了一些局部變量和GenerateInstructions方法。instructionSequence變量用來存儲一系列的UIElements對象,這些對象組成了Simon的指令集合。游戲者必須用手依次移動到這些指令上。這些指令的順序是隨機設定的。每一關指令的個數和當前等級是一樣的。比如,到了第五關,就有5個指令。代碼也顯示了DisplayInstruction方法,他創建並觸發了一個故事板動畫效果來根據指令的順序來改變每一個矩形的透明度。

private UIElement[] instructionSequence;
private int instructionPosition;
private int currentLevel;
private Random rnd = new Random();
private void GenerateInstructions()
{
    this.instructionSequence = new UIElement[this.currentLevel];
    
    for (int i = 0; i < this.currentLevel; i++)
    {
        switch (rnd.Next(1, 4))
        {
            case 1:
                this.instructionSequence[i] = RedBlock;
                break;
    
            case 2:
                this.instructionSequence[i] = BlueBlock;
                break;
    
            case 3:
                this.instructionSequence[i] = GreenBlock;
                break;
    
            case 4:
                this.instructionSequence[i] = YellowBlock;
                break;
        }
    }
}
    
private void DisplayInstructions()
{
    Storyboard instructionsSequence = new Storyboard();
    DoubleAnimationUsingKeyFrames animation;
    
    for (int i = 0; i < this.instructionSequence.Length; i++)
    {
        this.instructionSequence[i].ApplyAnimationClock(FrameworkElement.OpacityProperty, null);
    
        animation = new DoubleAnimationUsingKeyFrames();
        animation.FillBehavior = FillBehavior.Stop;
        animation.BeginTime = TimeSpan.FromMilliseconds(i * 1500);
        Storyboard.SetTarget(animation, this.instructionSequence[i]);
        Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
        instructionsSequence.Children.Add(animation);
    
        animation.KeyFrames.Add(new EasingDoubleKeyFrame(0.3, KeyTime.FromTimeSpan(TimeSpan.Zero)));
        animation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(500))));
        animation.KeyFrames.Add(new EasingDoubleKeyFrame(1, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1000))));
        animation.KeyFrames.Add(new EasingDoubleKeyFrame(0.3, KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(1300))));
    }
    
    instructionsSequence.Completed += (s, e) => { ChangePhase(GamePhase.PlayerPerforming); };
    instructionsSequence.Begin(LayoutRoot);
}

運行程序,當雙手放到指定位置是,Simon游戲開始。

2.4.3 執行Simon的指令

游戲的最後一步就是根據指令來捕捉用戶的動作。注意到當故事版動畫完成了顯示Simon的指令後,程序調用ChangePhase方法使游戲進入PlayerPerforming階段。當在PlayerPerforming階段時,應用程序執行ProcessPlayerPerforming方法。表面上,實現該方法很簡單。邏輯是游戲者重復Simon給出的操作步驟,將手放在對應矩形上方。這和之前做的命中測試邏輯是一樣的。但是,和測試兩個靜態的UI對象不同,我們測試指令集合中的下一個指令對應的元素。下面的代碼展示的ProcessPlayerPerforming方法,編譯並運行就可以看到效果了,雖然能夠運行,但是它對用戶非常不友好。實際上,這個游戲不能玩。我們的用戶界面不完整。

private void ProcessPlayerPerforming(Skeleton skeleton)
{
    //Determine if user is hitting a target and if that target is in the correct sequence.
    UIElement correctTarget     = this._InstructionSequence[this._InstructionPosition];
    IInputElement leftTarget    = GetHitTarget(skeleton.Joints[JointType.HandLeft], GameCanvas);
    IInputElement rightTarget   = GetHitTarget(skeleton.Joints[JointType.HandRight], GameCanvas);
    
    if(leftTarget != null && rightTarget != null)
    {
        ChangePhase(GamePhase.GameOver);
    }
    else if(leftTarget == null && rightTarget == null)
    {
        //Do nothing - target found
    }
    else if((leftTarget == correctTarget && rightTarget == null) ||
            (rightTarget == correctTarget && leftTarget == null))
    {                
        this._InstructionPosition++;
    
        if(this._InstructionPosition >= this._InstructionSequence.Length)
        {
            ChangePhase(GamePhase.SimonInstructing);
        }
    }
    else
    {                    
        ChangePhase(GamePhase.GameOver);
    }
}

上面的代碼中,第一行獲取目標對象元素,即指令序列中的當前指令。然後執行命中測試,獲取左右手對應的命中元素。下面的代碼對這三個變量進行操作。如果兩只手都在UI元素上,游戲結束。我們的游戲很簡單,只能允許一次點擊一個矩形。當兩只手都不在UI元素上時,什麼都不做。如果一只手命中了期望的對象,我們就把當前指令步驟加1。當指令集合中還有其他指令時游戲繼續運行,直到完成了指令集合中的最後一個指令。當完成了最後一個指令後,游戲狀態又變為了SimonInstruction狀態,然後將游戲者帶入下一輪游戲。直到游戲者不能重復Simon指令而進入GameOver狀態。

查看本欄目

如果游戲者動作夠快,那麼上面代碼工作正常,因為只要用戶手進入到了可視化元素有效區域,那麼指令位置就會自增,游戲者在進入到下一個指令之前,沒有時間來從UI元素所在的空間上移除手。這麼快的速度不可能使得游戲者能夠闖過第二關。當游戲者成功的闖過第二關的指令後,游戲就會突然停止。

解決這個問題的辦法是在進入到下一個指令前等待,直到游戲者的手勢從UI界面上清除。這使得游戲者有機會能夠調整手勢的位置開始進入下一條指令,我們需要記錄用戶的手什麼時候進入和離開UI對象。

在WPF中,每一個UIElement對象都會在鼠標進入和離開其有效范圍內時觸發MouseEnter和MouseLeave事件。不幸的是,如前面所討論的,WPF本身並不支持Kinect產生的關節點數據和UI的直接交互,如果當關節點進入或者離開可視化元素時能夠觸發諸如JointEnter和JointLeave事件,那麼就簡單多了。既然不支持,那麼我們只有自己手動實現這個邏輯了。要實現一個可重用,優雅,並能像鼠標那樣能夠在底層追蹤關節點運動這樣的控件不太容易並且不容易做成通用的。我們只針對我們當前遇到的問題來實現這個功能。

要修正游戲中的這個問題比較容易。我們添加一系列成員變量來保存UI元素上的哪一個鼠標手勢最後懸停在上面。當用戶的手經過UI元素的上方時,更新這個變量。對於每一個新的骨骼數據幀。我們檢查游戲者手的位置,如果它離開了UI元素空間,那麼我們處理這個UI元素。下面的代碼展示了對上面ProcessPlayerPerforming方法的改進。改進的部分用粗體表示。

private IInputElement leftHandTarget;
private IInputElement rightHandTarget;
private void ProcessPlayerPerforming(Skeleton skeleton)
{
    //判斷用戶是否手勢是否在目標對象上面,且在指定中的正確順序
    UIElement correctTarget = this.instructionSequence[this.instructionPosition];
    IInputElement leftTarget = GetHitTarget(skeleton.Joints[JointType.HandLeft], GameCanvas);
    IInputElement rightTarget = GetHitTarget(skeleton.Joints[JointType.HandRight], GameCanvas);
    bool hasTargetChange = (leftTarget != this.leftHandTarget) || (rightTarget != this.rightHandTarget);
    
    if (hasTargetChange)
    {
        if (leftTarget != null && rightTarget != null)
        {
            ChangePhase(GamePhase.GameOver);
        }
        else if ((leftHandTarget == correctTarget && rightHandTarget == null) ||
                (rightHandTarget == correctTarget && leftHandTarget == null))
        {
            this.instructionPosition++;
    
            if (this.instructionPosition >= this.instructionSequence.Length)
            {
                ChangePhase(GamePhase.SimonInstructing);
            }
        }
        else if (leftTarget != null || rightTarget != null)
        {
            //Do nothing - target found
        }
        else
        {
            ChangePhase(GamePhase.GameOver);
        }
    
        if (leftTarget != this.leftHandTarget)
        {
            if (this.leftHandTarget != null)
            {
                ((FrameworkElement)this.leftHandTarget).Opacity = 0.2;
            }
    
            if (leftTarget != null)
            {
                ((FrameworkElement)leftTarget).Opacity = 1;
            }
    
            this.leftHandTarget = leftTarget;
        }
    
        if (rightTarget != this.rightHandTarget)
        {
            if (this.rightHandTarget != null)
            {
                ((FrameworkElement)this.rightHandTarget).Opacity = 0.2;
            }
    
            if (rightTarget != null)
            {
                ((FrameworkElement)rightTarget).Opacity = 1;
            }
    
            this.rightHandTarget = rightTarget;
        }
    }
}

現在運行代碼,由於游戲需要兩只手進行操作,所以沒法截圖,讀者可以自己下載代碼運行。

2.5 需要改進的地方

這個游戲演示了如何建立一個基於Kinect進行交互的程序,雖然程序可以運行,但是仍然有一些有待改進的地方,有以下三個方面可以進行改進:用戶體驗,游戲內容和表現形式。

2.5.1 用戶體驗

基於Kinect的應用程序和游戲比較新穎,在這種應用達到成熟前,要想獲得良好的用戶體驗需要進行很多實驗和測試。我們的Simon Say游戲的用戶界面就有很多值得改進的地方。Simon Say的游戲者可能會意外的觸摸到游戲的區間。游戲時在游戲開始的時候,有可能會碰到開始按鈕。一旦兩只手都在指定的區間,游戲就開始產生指令,如果用戶沒有及時的放開手,他可能會無意識的碰到一個游戲對象。一個有效的解決方法是在產生指令之前,給予用戶一定的時間讓其重新設置手的位置。因為人們會自然而然的將手垂在身體兩邊。一個比較好的變通方法是簡單的給一個倒計時。在不同的關卡間,也可以給這樣一個時間間隔。在開始新的一關時,用戶應該有時間來從可視化元素中移開手。

2.5.2 游戲內容

產生游戲指令序列的邏輯比較簡單。指令序列中指令的數目和當前的關卡是一致的。每一條指令所選擇的可視化元素是隨機選擇的。在原始的Simon Say游戲中,新一輪的游戲通常會添加一些新的指令。例如,第一關中有紅的,第二關中有紅的和藍的,第三關增加了綠的。因此在第三關指令可以是,紅綠藍。另一種改進可以不在每一關增加一個指令。而是將指令的個數設置為當前關卡數的2倍。軟件開發一個有趣的地方就是應用程序可以有多種產生指令序列的算法。例如,應用程序可以分為容易,中等,難三種產生指令序列的方法供用戶選擇。最基本的產生指令序列的邏輯就是每一關要盡可能的比前一關要長,並且指令顯示速度要以一個常量的速度顯示。要增加游戲的難度,在顯示指令序列時可以減少指令展示給用戶的時間。

2.5.3表現形式

創建一個賦予表現力的程序遠不止我們這裡所介紹的這些內容。可能做一點改動就可以將我們的UI做的更加好看,比如,可以在顯示指令提示,以及用戶移入和離開指定區域時可以采用一些比較好看的動畫。當用戶執行的指令正確時,可以展現一個動畫效果給予獎勵。同樣的,在游戲結束時也可以展現出一個動畫。

3. 結語

本文圍繞Kinect介紹了WPF輸入系統的相關知識,並討論了如何將Kinect作為WPF程序的輸入設備與應用程序進行交互,最後展示了一個Simon say的小游戲來講述如何進行這些實際操作。

限於篇幅,下面一篇文章將會對骨骼追蹤進行進一步闡述,並對Simon say這個小游戲增加姿勢識別,敬請期待。

作者:   yangecnu(yangecnu's Blog on 博客園)

出處:http://www.cnblogs.com/yangecnu/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved