新增控制項

注意

本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。

[已針對 Windows 10 的 UWP 應用程式進行更新。 如需 Windows 8.x 相關文章,請參閱封存檔案]

理想的通用 Windows 平台 (UWP) 遊戲可支援各種介面。 潛在玩家使用 Windows 10 的裝置可能是:沒有實體按鍵的平板電腦、連接遊戲控制器的電腦,或搭配高效能滑鼠和遊戲鍵盤的最新桌面遊戲系統。 在我們的遊戲中,控制項是在 MoveLookController 類別中實作。 此類別會將所有三種輸入 (滑鼠和鍵盤、觸控和遊戲台) 彙總為單一控制器。 成品將會是第一人稱射擊遊戲,使用一般標準外觀視角控制項,適用於多種裝置。

注意

如需有關控制項的詳細資訊,請參閱<遊戲的移動視角控制項>和<遊戲的觸控控制項>。

目標

截至目前,我們已有一個可轉譯的遊戲,但無法移動玩家或射擊目標。 本文將說明如何針對 UWP DirectX 遊戲的下列輸入類型,實作第一人稱射擊的移動視角控制項。

  • 滑鼠和鍵盤
  • 觸控
  • 遊戲台

注意

如果您尚未下載此範例的最新遊戲程式碼,請至 Direct3D 範例遊戲頁面下載。 此範例屬於大型 UWP 功能範例集。 如需範例下載的相關指示,請參閱<適用 Windows 開發的範例應用程式>。

常見的控制項和行為

觸控控制項和滑鼠/鍵盤控制項的核心實作非常類似。 在 UWP 應用程式中,指標只是螢幕上的一個點。 移動滑鼠或在觸控螢幕上滑動手指,即可移動指標。 因此您可註冊一組事件,這樣就不必擔心玩家移動及按下指標時,使用的是滑鼠還是觸控螢幕。

初始化範例遊戲中的 MoveLookController 類別時,系統會註冊四個指標特定事件及一個滑鼠特定事件:

事件 描述
CoreWindow::PointerPressed 按下滑鼠左鍵或右鍵 (不放),或點選觸控介面。
CoreWindow::PointerMoved 在觸控介面移動滑鼠或拖曳動作。
CoreWindow::PointerReleased 已放開滑鼠左鍵或移開接觸觸控介面的物體。
CoreWindow::PointerExited 指標已移出主視窗。
Windows::Devices::Input::MouseMoved 滑鼠移動一段特定距離。 請注意,我們只需要滑鼠的移動差異值,而不是目前的 X-Y 位置。

這些事件處理常式已設為一旦 MoveLookController 在應用程式視窗中初始化,就開始接聽使用者輸入。

void MoveLookController::InitWindow(_In_ CoreWindow const& window)
{
    ResetState();

    window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

    window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

    window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

    window.PointerExited({ this, &MoveLookController::OnPointerExited });

    ...

    // There is a separate handler for mouse-only relative mouse movement events.
    MouseDevice::GetForCurrentView().MouseMoved({ this, &MoveLookController::OnMouseMoved });

    ...
}

在 GitHub 上可查看完整的 InitWindow 的程式碼。

為判斷遊戲何時應接聽特定輸入,MoveLookController 類別有三個控制器特定狀態 (不論控制器類型為何):

State 描述
None 這是控制器的初始化狀態。 由於遊戲未預期任何控制器輸入,因此會忽略所有輸入。
WaitForInput 控制器正在等候玩家以滑鼠左鍵、觸控事件或遊戲台的功能表按鍵確認遊戲訊息。
使用中 控制器處於使用中的遊戲模式。

WaitForInput 狀態和暫停遊戲

遊戲在暫停時進入 WaitForInput 狀態。 玩家將指標移至遊戲主視窗外,或按下暫停按鈕 (P 鍵或遊戲台的 [開始] 按鍵),就會發生這種情況。 MoveLookController 會註冊該按下事件,並在呼叫 IsPauseRequested 方法時通知遊戲迴圈。 此時如果 IsPauseRequested 傳回true,遊戲迴圈接著會對 MoveLookController 呼叫 WaitForPress,使控制器處於 WaitForInput 狀態。

一旦處於 WaitForInput 狀態,遊戲會停止處理幾乎所有遊戲輸入事件,直到其回到 Active 狀態。 除非按下暫停按鈕,按下該按鈕會使遊戲回到使用中狀態。 除了暫停按鈕,如要讓遊戲回到 Active 狀態,玩家需要選取功能表項目。

Active (使用中) 狀態

Active 狀態期間,MoveLookController 執行個體會處理來自所有已啟用輸入裝置的事件,並解譯玩家的意圖。 因此,該執行個體會更新玩家檢視的速度和視角方向,並在從遊戲迴圈呼叫 Update 後,與遊戲共用已更新的資料。

所有指標輸入都在 Active 狀態下受到追蹤,並使用不同的指標識別碼對應不同的指標動作。 收到 PointerPressed 事件時,MoveLookController 會取得視窗建立的指標識別碼值。 指標識別碼代表特定類型的輸入。 例如,在多點觸控裝置上,同時可能有數個不同的輸入處於使用中狀態。 識別碼可用來追蹤玩家使用的輸入。 如果某個事件位於觸控螢幕的移動矩形中,系統會指派識別碼以追蹤移動矩形中的任何指標事件。 系統會以不同的指標識別碼個別追蹤觸發矩形中的其他指標事件。

注意

滑鼠和遊戲台右搖桿的輸入也有識別碼並個別處理。

將指標事件對應至特定遊戲動作後,即可更新 MoveLookController 物件與主要遊戲迴圈共用的資料。

呼叫範例遊戲中的 Update 方法時,系統會處理輸入並更新速度和視角方向變數 (m_velocitym_lookdirection),接著遊戲迴圈會擷取這些資料,方法是呼叫 VelocityLookDirection 公用方法。

注意

如需有關 Update 方法的詳細資料,本頁稍後會進一步說明。

遊戲迴圈可對 MoveLookController 執行個體呼叫 IsFiring 方法,測試玩家是否觸發動作。 MoveLookController 會檢查玩家是否已按下三種輸入的任一種觸發按鈕。

bool MoveLookController::IsFiring()
{
    if (m_state == MoveLookControllerState::Active)
    {
        if (m_autoFire)
        {
            return (m_fireInUse || (m_mouseInUse && m_mouseLeftInUse) || PollingFireInUse());
        }
        else
        {
            if (m_firePressed)
            {
                m_firePressed = false;
                return true;
            }
        }
    }
    return false;
}

接著,我們一起更深入瞭解這三種控制類型的實作方式。

新增相對滑鼠控制項

如果偵測到滑鼠移動,則可使用該動作判斷相機的新俯仰和偏航維度。 我們可透過實作相對滑鼠控制項執行此動作,在此情況下處理的是滑鼠移動的相對距離 (移動開始到停止之間的差異值),而非記錄動作的絕對 x-y 像素座標。

如要執行此動作,我們可在 MouseMoved 事件傳回的 Windows::Device::Input::MouseEventArgs::MouseDelta 引數物件中,檢視 MouseDelta::XMouseDelta::Y 欄位,取得 X (水平動作) 的變化值及 Y (垂直動作) 座標。

void MoveLookController::OnMouseMoved(
    _In_ MouseDevice const& /* mouseDevice */,
    _In_ MouseEventArgs const& args
    )
{
    // Handle Mouse Input via dedicated relative movement handler.

    switch (m_state)
    {
    case MoveLookControllerState::Active:
        XMFLOAT2 mouseDelta;
        mouseDelta.x = static_cast<float>(args.MouseDelta().X);
        mouseDelta.y = static_cast<float>(args.MouseDelta().Y);

        XMFLOAT2 rotationDelta;
        // Scale for control sensitivity.
        rotationDelta.x = mouseDelta.x * MoveLookConstants::RotationGain;
        rotationDelta.y = mouseDelta.y * MoveLookConstants::RotationGain;

        // Update our orientation based on the command.
        m_pitch -= rotationDelta.y;
        m_yaw += rotationDelta.x;

        // Limit pitch to straight up or straight down.
        float limit = XM_PI / 2.0f - 0.01f;
        m_pitch = __max(-limit, m_pitch);
        m_pitch = __min(+limit, m_pitch);

        // Keep longitude in sane range by wrapping.
        if (m_yaw > XM_PI)
        {
            m_yaw -= XM_PI * 2.0f;
        }
        else if (m_yaw < -XM_PI)
        {
            m_yaw += XM_PI * 2.0f;
        }
        break;
    }
}

新增觸控支援

觸控控制項很適合支援平板電腦使用者。 此遊戲會透過區分畫面的特定區域收集觸控輸入,各區域會對應特定的遊戲內動作。 此遊戲的觸控輸入使用三個區域。

move look touch layout

下列命令摘要說明觸控控制行為。 User input | Action :------- | :-------- Move rectangle | 觸控輸入會轉換成虛擬搖桿,其中垂直動作會轉譯為向前/向後位置動作,而水平動作會轉譯為向左/向右位置動作。 觸發矩形 | 觸發球體。 發生在移動和觸發矩形外的觸控 | 變更相機檢視的旋轉角度 (俯仰和偏航)。

MoveLookController 會檢查識別碼 ID,以判斷事件發生位置並執行下列任一動作:

  • 如果 PointerMoved 事件發生在移動或觸發矩形內,請更新控制器的指標位置。
  • 如果 PointerMoved 事件發生在畫面 (如視角控制項所定義) 其餘部分某處,則計算視角方向向量的俯仰和偏航變化。

實作觸控控制項後,稍早以 Direct2D 繪製的矩形就會向玩家指示移動、觸發和視角區的位置。

touch controls

現在,我們來看看如何實作每個控制項。

移動和觸發控制器

螢幕左下象限中的移動控制器矩形可當成方向鍵使用。 在這個空間中,左右滑動拇指可使玩家左右移動,拇指上下滑動則可使相機前後移動。 完成此設定之後,點選螢幕右下象限中的觸發控制器可觸發球體。

SetMoveRectSetFireRect 方法會建立輸入矩形,並採用兩個 2D 向量指定螢幕上每個矩形的左上角和右下角位置。

然後,系統會指派參數給 m_fireUpperLeftm_fireLowerRight,以協助判斷使用者觸控的位置是否在矩形內。

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

如果螢幕調整大小,這些矩形也會重新繪製為適當大小。

我們已區隔控制項區域,接著可判斷使用者實際使用控制項的時間。 針對使用者按下、移動或放開指標的動作,在 MoveLookController::InitWindow 方法中設定一些事件處理常式,即可做到這點。

window.PointerPressed({ this, &MoveLookController::OnPointerPressed });

window.PointerMoved({ this, &MoveLookController::OnPointerMoved });

window.PointerReleased({ this, &MoveLookController::OnPointerReleased });

我們先使用 OnPointerPressed 方法,判斷使用者在移動或觸發矩形內第一次按下控制項時發生的狀況。 此時,我們會檢查使用者點選控制項的位置,以及指標是否已在該控制器中。 如果是點選特定控制項的第一根手指,我們會執行下列動作。

  • m_moveFirstDownm_fireFirstDown 中將觸控位置儲存為 2D 向量。
  • 指派指標識別碼給 m_movePointerIDm_firePointerID
  • 將適當的 InUse 旗標 (m_moveInUsem_fireInUse) 設為 true,因為現在已有該控制項的使用者指標。
PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();
auto pointerDeviceType = pointerDevice.PointerDeviceType();

XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

...
case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Check to see if this pointer is in the move control.
        if (position.x > m_moveUpperLeft.x &&
            position.x < m_moveLowerRight.x &&
            position.y > m_moveUpperLeft.y &&
            position.y < m_moveLowerRight.y)
        {
            // If no pointer is in this control yet.
            if (!m_moveInUse)
            {
                // Process a DPad touch down event.
                // Save the location of the initial contact
                m_moveFirstDown = position;
                // Store the pointer using this control
                m_movePointerID = pointerID;
                // Set InUse flag to signal there is an active move pointer
                m_moveInUse = true;
            }
        }
        // Check to see if this pointer is in the fire control.
        else if (position.x > m_fireUpperLeft.x &&
            position.x < m_fireLowerRight.x &&
            position.y > m_fireUpperLeft.y &&
            position.y < m_fireLowerRight.y)
        {
            if (!m_fireInUse)
            {
                // Save the location of the initial contact
                m_fireLastPoint = position;
                // Store the pointer using this control
                m_firePointerID = pointerID;
                // Set InUse flag to signal there is an active fire pointer
                m_fireInUse = true;
                ...
            }
        }
        ...

我們已經確認使用者是否點選移動或觸發控制項,接著可判斷玩家按下控制項後是否進行任何移動。 使用 MoveLookController::OnPointerMoved 方法檢查指標已移動的項目,然後將其新位置儲存為 2D 向量。

PointerPoint point = args.CurrentPoint();
uint32_t pointerID = point.PointerId();
Point pointerPosition = point.Position();
PointerPointProperties pointProperties = point.Properties();
auto pointerDevice = point.PointerDevice();

// convert to allow math
XMFLOAT2 position = XMFLOAT2(pointerPosition.X, pointerPosition.Y);

switch (m_state)
{
case MoveLookControllerState::Active:
    // Decide which control this pointer is operating.

    // Move control
    if (pointerID == m_movePointerID)
    {
        // Save the current position.
        m_movePointerPosition = position;
    }
    // Look control
    else if (pointerID == m_lookPointerID)
    {
        ...
    }
    // Fire control
    else if (pointerID == m_firePointerID)
    {
        m_fireLastPoint = position;
    }
    ...

使用者在控制項內做出手勢後,就會放開指標。 使用 MoveLookController::OnPointerReleased 方法,判斷哪個指標已放開並進行一系列的重設。

如果移動控制項已放開,我們會執行下列動作。

  • 將玩家速度的所有方向都設為 0,防止他們在遊戲中移動。
  • m_moveInUse 切換至 false,因為使用者不再觸控移動控制器。
  • 將移動指標識別碼設為 0,因為移動控制器中不再有指標。
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

如果觸發控制項已放開,我們只需要將 m_fireInUse 旗標切換至 false,並將觸發指標識別碼設為 0 即可,因為觸發控制項不再有指標。

else if (pointerID == m_firePointerID)
{
    m_fireInUse = false;
    m_firePointerID = 0;
}

視角控制器

我們會將螢幕未使用區域中的觸控裝置指標事件視為視角控制器。 在這個區域滑動手指會改變玩家相機的俯仰和偏航 (旋轉) 角度。

如果此區域的觸控裝置上引發了 MoveLookController::OnPointerPressed 事件,且遊戲狀態設為 Active,系統會指派指標識別碼。

// If no pointer is in this control yet.
if (!m_lookInUse)
{
    // Save point for later move.
    m_lookLastPoint = position;
    // Store the pointer using this control.
    m_lookPointerID = pointerID;
    // These are for smoothing.
    m_lookLastDelta.x = m_lookLastDelta.y = 0;
    m_lookInUse = true;
}

此時,對於引發事件的指標,MoveLookController 會指派指標識別碼給對應視角區域的特定變數。 若視角區域發生觸控事件,m_lookPointerID 變數會設為觸發事件的指標識別碼。 布林值變數 m_lookInUse 也會設為指示控制項尚未放開。

接著,我們來看看範例遊戲如何處理 PointerMoved 觸控螢幕事件。

MoveLookController::OnPointerMoved 方法內,我們會檢查是否已指派指標識別碼給事件。 如果是 m_lookPointerID,則計算指標位置變更。 然後使用該差異值計算旋轉的變更值。 最後,我們可更新要在遊戲中使用的 m_pitchm_yaw,以變更玩家的旋轉角度。

// This is the look pointer.
else if (pointerID == m_lookPointerID)
{
    // Look control.
    XMFLOAT2 pointerDelta;
    // How far did the pointer move?
    pointerDelta.x = position.x - m_lookLastPoint.x;
    pointerDelta.y = position.y - m_lookLastPoint.y;

    XMFLOAT2 rotationDelta;
    // Scale for control sensitivity.
    rotationDelta.x = pointerDelta.x * MoveLookConstants::RotationGain;
    rotationDelta.y = pointerDelta.y * MoveLookConstants::RotationGain;
    // Save for next time through.
    m_lookLastPoint = position;

    // Update our orientation based on the command.
    m_pitch -= rotationDelta.y;
    m_yaw += rotationDelta.x;

    // Limit pitch to straight up or straight down.
    float limit = XM_PI / 2.0f - 0.01f;
    m_pitch = __max(-limit, m_pitch);
    m_pitch = __min(+limit, m_pitch);
    ...
}

最後要介紹的部分是範例遊戲如何處理 PointerReleased 觸控螢幕事件。 使用者完成觸控手勢並將手指從螢幕上移開後,即啟動 MoveLookController::OnPointerReleased。 如果觸發 PointerReleased 事件的指標識別碼就是先前記錄的移動指標識別碼,MoveLookController 會將速度設為 0,因為玩家已停止觸控視角區域。

else if (pointerID == m_lookPointerID)
{
    m_lookInUse = false;
    m_lookPointerID = 0;
}

新增滑鼠和鍵盤支援

此遊戲具有以下鍵盤和滑鼠控制項版面配置。

使用者輸入 動作
星期三 將玩家往前移
A 將玩家往左移
S 將玩家往後移
D 將玩家往右移
X 向上移動檢視
空格鍵 向下移動檢視
P 暫停遊戲
滑鼠移動 變更相機檢視的旋轉 (俯仰和偏航)
滑鼠左鍵 觸發球體

為使用鍵盤,範例遊戲在 MoveLookController::InitWindow 方法中註冊 CoreWindow::KeyUpCoreWindow::KeyDown 兩個新事件。 這些事件會處理按鍵的按下和放開。

window.KeyDown({ this, &MoveLookController::OnKeyDown });

window.KeyUp({ this, &MoveLookController::OnKeyUp });

即使滑鼠也使用指標,但系統處理滑鼠的方式還是會與處理觸控控制項的方式略有不同。 為配合控制項的版面配置,MoveLookController 會在滑鼠移動時旋轉相機,並於使用者按下滑鼠左鍵時觸發。

這會在 MoveLookControllerOnPointerPressed 方法中進行處理。

在此方法中,我們會檢查與 Windows::Devices::Input::PointerDeviceType 列舉搭配使用的指標裝置類型。 如果遊戲處於 Active 狀態,但 PointerDeviceType 的值不是 Touch,則假設其為滑鼠輸入。

case MoveLookControllerState::Active:
    switch (pointerDeviceType)
    {
    case winrt::Windows::Devices::Input::PointerDeviceType::Touch:
        // Behavior for touch controls
        ...

    default:
        // Behavior for mouse controls
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        if (!m_autoFire && (!m_mouseLeftInUse && leftButton))
        {
            m_firePressed = true;
        }

        if (!m_mouseInUse)
        {
            m_mouseInUse = true;
            m_mouseLastPoint = position;
            m_mousePointerID = pointerID;
            m_mouseLeftInUse = leftButton;
            m_mouseRightInUse = rightButton;
            // These are for smoothing.
            m_lookLastDelta.x = m_lookLastDelta.y = 0;
        }
        break;
    }
    break;

當玩家停止按下其中一個滑鼠按鈕,會引發 CoreWindow::PointerReleased 滑鼠事件,並呼叫 MoveLookController::OnPointerReleased 方法,這樣即輸入完成。 此時如果滑鼠左鍵原本是按下狀態,而現在已放開,球體就會停止觸發。 由於視角一律已啟用,遊戲會繼續使用相同的滑鼠指標,追蹤進行中的視角事件。

case MoveLookControllerState::Active:
    // Touch points
    if (pointerID == m_movePointerID)
    {
        // Stop movement
        ...
    }
    else if (pointerID == m_lookPointerID)
    {
        // Stop look rotation
        ...
    }
    // Fire button has been released
    else if (pointerID == m_firePointerID)
    {
        // Stop firing
        ...
    }
    // Mouse point
    else if (pointerID == m_mousePointerID)
    {
        bool rightButton = pointProperties.IsRightButtonPressed();
        bool leftButton = pointProperties.IsLeftButtonPressed();

        // Mouse no longer in use so stop firing
        m_mouseInUse = false;

        // Don't clear the mouse pointer ID so that Move events still result in Look changes.
        // m_mousePointerID = 0;
        m_mouseLeftInUse = leftButton;
        m_mouseRightInUse = rightButton;
    }
    break;

接著我們來看看最後一個支援控制項類型:遊戲台。 由於遊戲台不使用指標物件,處理方式與觸控和滑鼠控制項不同, 因此需要新增一些新的事件處理常式和方法。

新增遊戲台支援

針對此遊戲,呼叫 Windows.Gaming.Input API 即可新增遊戲台支援。 這組 API 可讓您存取遊戲控制器輸入,例如賽車方向盤和飛行搖桿。

遊戲台控制項如下所列。

使用者輸入 動作
左模擬搖桿 移動玩家
右模擬搖桿 變更相機檢視的旋轉 (俯仰和偏航)
觸發往右 觸發球體
[開始/功能表] 按鍵 暫停或繼續遊戲

InitWindow 方法中,我們會新增兩個新事件,以判斷遊戲台是否已新增移除。 這些事件會更新 m_gamepadsChanged 屬性。 該屬性用於 UpdatePollingDevices 方法,以檢查已知遊戲台清單是否已變更。

// Detect gamepad connection and disconnection events.
Gamepad::GamepadAdded({ this, &MoveLookController::OnGamepadAdded });

Gamepad::GamepadRemoved({ this, &MoveLookController::OnGamepadRemoved });

注意

焦點不在 UWP 應用程式上時,該應用程式無法從遊戲控制器接收輸入。

UpdatePollingDevices 方法

MoveLookController 執行個體的 UpdatePollingDevices 方法會立即檢查遊戲台是否已連線。 如果已連線,則使用 Gamepad.GetCurrentReading 開始讀取其狀態。 這會傳回 GamepadReading 結構,讓我們檢查哪些按鈕已按下或已移動哪些搖桿。

如果遊戲的狀態是 WaitForInput,我們只會接聽控制器的 [開始/功能表] 按鍵,使遊戲繼續進行。

如果是 Active 狀態,我們會檢查使用者輸入,並判斷需要執行哪些遊戲內動作。 例如,如果使用者向特定方向移動左模擬搖桿,這可讓遊戲知道需要將玩家移往搖桿移動的方向。 對於往特定方向移動搖桿的動作,必須註冊大於死區的半徑,否則不會有任何反應。 必須透過這個死區半徑來防止「漂移」,也就是控制器擷取到玩家停留在搖桿上的拇指有任何小動作時。 如果沒有死區,使用者可能會覺得控制項太過靈敏。

X 軸和 y 軸的搖桿輸入都是介於 -1 到 1 之間。 下列常數指定搖桿的死區半徑。

#define THUMBSTICK_DEADZONE 0.25f

接著,我們會使用此變數開始處理可操作的搖桿輸入。 移動會發生在任一軸上 [-1, -.26] 或 [.26, 1] 的值。

dead zone for thumbsticks

UpdatePollingDevices 方法的這部分將處理左搖桿及右搖桿。 系統會檢查每個搖桿的 X 和 Y 值是否位於死區之外。 若其中一個或兩者都在範圍外,則更新對應的元件。 例如,如果左搖桿沿著 X 軸向左移動,我們會將 -1 新增至 m_moveCommand 向量的 x 元件。 此向量將用來彙整所有裝置上的所有移動,稍後用於計算玩家應移動的位置。

// Use the left thumbstick to control the eye point position
// (position of the player).

// Check if left thumbstick is outside of dead zone on x axis
if (reading.LeftThumbstickX > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickX < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on x axis
    float x = static_cast<float>(reading.LeftThumbstickX);
    // Set the x of the move vector to 1 if the stick is being moved right.
    // Set to -1 if moved left. 
    m_moveCommand.x -= (x > 0) ? 1 : -1;
}

// Check if left thumbstick is outside of dead zone on y axis
if (reading.LeftThumbstickY > THUMBSTICK_DEADZONE ||
    reading.LeftThumbstickY < -THUMBSTICK_DEADZONE)
{
    // Get value of left thumbstick's position on y axis
    float y = static_cast<float>(reading.LeftThumbstickY);
    // Set the y of the move vector to 1 if the stick is being moved forward.
    // Set to -1 if moved backwards.
    m_moveCommand.y += (y > 0) ? 1 : -1;
}

與左搖桿控制移動的方式類似,右搖桿控制相機旋轉。

右搖桿行為與滑鼠和鍵盤控制項設定中的滑鼠移動行為一致。 如果遊戲桿位於死區之外,則針對目前指標位置與使用者當下嘗試查看的位置,計算兩者差異。 這項指標位置的變更值 (pointerDelta) 接著會用於更新相機旋轉的俯仰和偏航,這些資料稍後將用於 Update 方法。 pointerDelta 向量看起來可能很熟悉,因為其亦用於 MoveLookController::OnPointerMoved 方法,以追蹤滑鼠和觸控輸入的指標位置變更。

// Use the right thumbstick to control the look at position

XMFLOAT2 pointerDelta;

// Check if right thumbstick is outside of deadzone on x axis
if (reading.RightThumbstickX > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickX < -THUMBSTICK_DEADZONE)
{
    float x = static_cast<float>(reading.RightThumbstickX);
    // Register the change in the pointer along the x axis
    pointerDelta.x = x * x * x;
}
// No actionable thumbstick movement. Register no change in pointer.
else
{
    pointerDelta.x = 0.0f;
}
// Check if right thumbstick is outside of deadzone on y axis
if (reading.RightThumbstickY > THUMBSTICK_DEADZONE ||
    reading.RightThumbstickY < -THUMBSTICK_DEADZONE)
{
    float y = static_cast<float>(reading.RightThumbstickY);
    // Register the change in the pointer along the y axis
    pointerDelta.y = y * y * y;
}
else
{
    pointerDelta.y = 0.0f;
}

XMFLOAT2 rotationDelta;
// Scale for control sensitivity.
rotationDelta.x = pointerDelta.x * 0.08f;
rotationDelta.y = pointerDelta.y * 0.08f;

// Update our orientation based on the command.
m_pitch += rotationDelta.y;
m_yaw += rotationDelta.x;

// Limit pitch to straight up or straight down.
m_pitch = __max(-XM_PI / 2.0f, m_pitch);
m_pitch = __min(+XM_PI / 2.0f, m_pitch);

如果不能觸發球體,遊戲的控制項就不會完成!

UpdatePollingDevices 方法也會檢查是否已按下正確的觸發程序。 如果是,m_firePressed 屬性會翻轉為 true,並向遊戲發出訊號指出應開始觸發球體。

if (reading.RightTrigger > TRIGGER_DEADZONE)
{
    if (!m_autoFire && !m_gamepadTriggerInUse)
    {
        m_firePressed = true;
    }

    m_gamepadTriggerInUse = true;
}
else
{
    m_gamepadTriggerInUse = false;
}

Update 方法

最後,我們來進一步瞭解 Update 方法。 此方法會合併玩家使用任何支援輸入進行的任何移動或旋轉,以產生速度向量並更新俯仰和偏航值,供遊戲迴圈存取。

Update 方法會呼叫 UpdatePollingDevices 啟動動作,並更新控制器的狀態。 此方法也會收集來自遊戲台的任何輸入,並將其移動新增至 m_moveCommand 向量。

Update 方法中接著執行下列輸入檢查。

  • 如果玩家使用移動控制器矩形,我們接下來要判斷指標位置的變更,並使用該值計算使用者是否已將指標移出控制器的死區。 如果超出死區,m_moveCommand 向量屬性就會更新為虛擬搖桿值。
  • 如果按下任何移動鍵盤輸入,則 1.0f-1.0f的值會新增至 m_moveCommand 往前向量 (1.0f) 及往後 (-1.0f) 向量的對應元件。

考量所有移動輸入之後,我們接著會透過一些計算執行 m_moveCommand 向量,以產生新向量,代表玩家在遊戲事件的方向。 然後採取與世界有關的動作,並將其視為該方向的速度套用至玩家。 最後,我們會將 m_moveCommand 向量重設為 (0.0f, 0.0f, 0.0f),確保所有資源已準備好用於下一個遊戲畫面格。

void MoveLookController::Update()
{
    // Get any gamepad input and update state
    UpdatePollingDevices();

    if (m_moveInUse)
    {
        // Move control.
        XMFLOAT2 pointerDelta;

        pointerDelta.x = m_movePointerPosition.x - m_moveFirstDown.x;
        pointerDelta.y = m_movePointerPosition.y - m_moveFirstDown.y;

        // Figure out the command from the virtual joystick.
        XMFLOAT3 commandDirection = XMFLOAT3(0.0f, 0.0f, 0.0f);
        // Leave 32 pixel-wide dead spot for being still.
        if (fabsf(pointerDelta.x) > 16.0f)
            m_moveCommand.x -= pointerDelta.x / fabsf(pointerDelta.x);

        if (fabsf(pointerDelta.y) > 16.0f)
            m_moveCommand.y -= pointerDelta.y / fabsf(pointerDelta.y);
    }

    // Poll our state bits set by the keyboard input events.
    if (m_forward)
    {
        m_moveCommand.y += 1.0f;
    }
    if (m_back)
    {
        m_moveCommand.y -= 1.0f;
    }
    if (m_left)
    {
        m_moveCommand.x += 1.0f;
    }
    if (m_right)
    {
        m_moveCommand.x -= 1.0f;
    }
    if (m_up)
    {
        m_moveCommand.z += 1.0f;
    }
    if (m_down)
    {
        m_moveCommand.z -= 1.0f;
    }

    // Make sure that 45deg cases are not faster.
    if (fabsf(m_moveCommand.x) > 0.1f ||
        fabsf(m_moveCommand.y) > 0.1f ||
        fabsf(m_moveCommand.z) > 0.1f)
    {
        XMStoreFloat3(&m_moveCommand, XMVector3Normalize(XMLoadFloat3(&m_moveCommand)));
    }

    // Rotate command to align with our direction (world coordinates).
    XMFLOAT3 wCommand;
    wCommand.x = m_moveCommand.x * cosf(m_yaw) - m_moveCommand.y * sinf(m_yaw);
    wCommand.y = m_moveCommand.x * sinf(m_yaw) + m_moveCommand.y * cosf(m_yaw);
    wCommand.z = m_moveCommand.z;

    // Scale for sensitivity adjustment.
    // Our velocity is based on the command. Y is up.
    m_velocity.x = -wCommand.x * MoveLookConstants::MovementGain;
    m_velocity.z = wCommand.y * MoveLookConstants::MovementGain;
    m_velocity.y = wCommand.z * MoveLookConstants::MovementGain;

    // Clear movement input accumulator for use during next frame.
    m_moveCommand = XMFLOAT3(0.0f, 0.0f, 0.0f);
}

下一步

新增完控制項之後,接著我們還需要新增另一項功能,才能打造沉浸式遊戲:音效! 音樂和音效對所有遊戲都不可或缺,因此接下來我們會說明如何新增音效