添加控件Add controls

备注

本主题是 使用 DirectX 教程系列 (UWP) 游戏创建简单通用 Windows 平台 的一部分。This topic is part of the Create a simple Universal Windows Platform (UWP) game with DirectX tutorial series. 该链接上的主题设置了序列的上下文。The topic at that link sets the context for the series.

[ 已针对 Windows 10 上的 UWP 应用进行更新。[ Updated for UWP apps on Windows 10. 有关 Windows 2.x 的文章,请参阅存档]For Windows 8.x articles, see the archive ]

优秀的通用 Windows 平台 (UWP) 游戏支持多种界面。A good Universal Windows Platform (UWP) game supports a wide variety of interfaces. 潜在玩家可能将 Windows 10 安装在没有物理按钮的平板电脑、连接了 Xbox 控制器的电脑,或者具有高性能鼠标和游戏键盘的最新桌面游戏设备上。A potential player might have Windows 10 on a tablet with no physical buttons, a PC with an Xbox controller attached, or the latest desktop gaming rig with a high-performance mouse and gaming keyboard. 在我们的游戏中,控制在 MoveLookController 类中实现。In our game the controls are implemented in the MoveLookController class. 此类将全部三个输入类型(鼠标和键盘、触控和游戏板)聚合到一个控制器内。This class aggregates all three types of input (mouse and keyboard, touch, and gamepad) into a single controller. 最终结果是一个第一人称射击游戏,其使用通过多台设备使用的流派标准移动观看控件。The end result is a first-person shooter that uses genre standard move-look controls that work with multiple devices.

备注

有关控件的详细信息,请参阅游戏的移动观看控件游戏的触摸控件For more info about controls, see Move-look controls for games and Touch controls for games.

目标Objective

此时,我们有呈现的游戏,但我们不能四处移动玩家或射击目标。At this point we have a game that renders, but we can't move our player around or shoot the targets. 我们来看看我们的游戏如何在 UWP DirectX 游戏中为以下类型的输入实现第一人称射击游戏移动观看控件。We'll take a look at how our game implements first person shooter move-look controls for the following types of input in our UWP DirectX game.

  • 鼠标和键盘Mouse and keyboard
  • 触摸Touch
  • 游戏板Gamepad

备注

如果尚未下载此示例的最新游戏代码,请参阅 Direct3D 示例游戏If you haven't downloaded the latest game code for this sample, go to Direct3D sample game. 此示例是大型 UWP 功能示例集合的一部分。This sample is part of a large collection of UWP feature samples. 有关如何下载示例的说明,请参阅从 GitHub 获取 UWP 示例For instructions on how to download the sample, see Get the UWP samples from GitHub.

常用控件行为Common control behaviors

触摸控件和鼠标/键盘控件具有非常类似的核心实现。Touch controls and mouse/keyboard controls have a very similar core implementation. 在 UWP 应用中,指针只是屏幕上的点。In a UWP app, a pointer is simply a point on the screen. 你可以滑动鼠标或在触摸屏上滑动手指来移动指针。You can move it by sliding the mouse or sliding your finger on the touch screen. 因此,你可以注册单个事件集,而不用担心玩家使用鼠标或触摸屏移动或点按指针。As a result, you can register for a single set of events, and not worry about whether the player is using a mouse or a touch screen to move and press the pointer.

当示例游戏中的 MoveLookController 类初始化时,它将注册四个特定于指针的事件和一个鼠标特定事件:When the MoveLookController class in the sample game is initialized, it registers for four pointer-specific events and one mouse-specific event:

事件Event 描述Description
CoreWindow::PointerPressedCoreWindow::PointerPressed 点按(并按住)左右鼠标按钮,或触摸触摸屏表面。The left or right mouse button was pressed (and held), or the touch surface was touched.
CoreWindow::PointerMovedCoreWindow::PointerMoved 移动鼠标,或在触摸表面执行拖动操作。The mouse moved, or a drag action was made on the touch surface.
CoreWindow::P ointerReleasedCoreWindow::PointerReleased 释放鼠标左按钮,或者移开与触摸表面接触的物体。The left mouse button was released, or the object contacting the touch surface was lifted.
CoreWindow::PointerExitedCoreWindow::PointerExited 指针移出主窗口。The pointer moved out of the main window.
Windows::Devices::Input::MouseMovedWindows::Devices::Input::MouseMoved 鼠标移动了一定距离。The mouse moved a certain distance. 注意,我们只关注鼠标移动增量值,不关注当前 X-Y 位置。Be aware that we are only interested in mouse movement delta values, and not the current X-Y position.

这些事件处理程序被设置为当 MoveLookController 在应用程序窗口中初始化后即为用户输入启动侦听。These event handlers are set to start listening for user input as soon as the MoveLookController is initialized in the application window.

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 });

    ...
}

InitWindow 的完整代码可以在 GitHub 上看到。Complete code for InitWindow can be seen on GitHub.

若要确定游戏应在何时侦听某个输入,MoveLookController 类有三个特定于控制器的状态(与控制器类型无关):To determine when the game should be listening for certain input, the MoveLookController class has three controller-specific states, regardless of the controller type:

状态State 说明Description
None 这是控制器的初始化状态。This is the initialized state for the controller. 忽略所有输入,因为游戏不期待任何控制器输入。All input is ignored since the game is not anticipating any controller input.
WaitForInputWaitForInput 控制器等待玩家通过使用鼠标左键、触摸事件或游戏板上的菜单按钮确认来自游戏的消息。The controller is waiting for the player to acknowledge a message from the game by either using a left mouse click, a touch event, ot the menu button on a gamepad.
活动Active 控制器处于活动的游戏模式。The controller is in active game play mode.

WaitForInput 状态和暂停游戏WaitForInput state and pausing the game

游戏在暂停后进入 WaitForInput 状态。The game enters the WaitForInput state when the game has been paused. 当玩家将指针移出游戏主窗口或者按下暂停按钮(P 键或游戏板开始按钮)时将发生此情况。This happens when the player moves the pointer outside the main window of the game, or presses the pause button (the P key or the gamepad Start button). MoveLookController 注册点按操作,并在它调用 IsPauseRequested 方法时通知游戏循环。The MoveLookController registers the press, and informs the game loop when it calls the IsPauseRequested method. 此时,如果 IsPauseRequested 返回 true,游戏循环将对 MoveLookController 调用 WaitForPress 以将控制器转入 WaitForInput 状态。At that point if IsPauseRequested returns true, the game loop then calls WaitForPress on the MoveLookController to move the controller into the WaitForInput state.

进入 WaitForInput 状态后,游戏将停止处理几乎所有游戏输入事件,直到它返回到 Active 状态。Once in the WaitForInput state, the game stops processing almost all gameplay input events until it returns to the Active state. 例外情况是暂停按钮,按此按钮会使游戏回退到活动状态。The exception is the pause button, with a press of this causing the game to go back to the active state. 除 "暂停" 按钮外,要使游戏返回到播放机的 活动 状态,播放机需要选择菜单项。Other than the pause button, in order for the game to go back to the Active state the player needs to select a menu item.

Active 状态The Active state

Active 状态期间,MoveLookController 实例处理来自所有启用的输入设备的事件,并解释玩家的意图。During the Active state, the MoveLookController instance is processing events from all enabled input devices and interpreting the player's intentions. 因此,在从游戏循环中调用 Update 之后,它会更新玩家视野的速度和观看方向,并与游戏共享更新数据。As a result, it updates the velocity and look direction of the player's view and shares the updated data with the game after Update is called from the game loop.

所有指针输入都在 Active 状态中跟踪,不同的指针 ID 对应于不同的指针操作。All pointer input is tracked in the Active state, with different pointer IDs corresponding to different pointer actions. 当收到 PointerPressed 事件时,MoveLookController 将获取窗口创建的指针 ID 值。When a PointerPressed event is received, the MoveLookController obtains the pointer ID value created by the window. 指针 ID 表示特定的输入类型。The pointer ID represents a specific type of input. 例如,在多点触控设备上,可能同时有若干不同的主动输入。For example, on a multi-touch device, there may be several different active inputs at the same time. ID 用于跟踪玩家在使用哪个输入。The IDs are used to keep track of which input the player is using. 如果触摸屏的移动矩形中有一个事件,将指派一个指针 ID 跟踪移动矩形中的任何指针事件。If one event is in the move rectangle of the touch screen, a pointer ID is assigned to track any pointer events in the move rectangle. 射击矩形中的其他指针事件则使用单独的指针 ID 进行单独跟踪。Other pointer events in the fire rectangle are tracked separately, with a separate pointer ID.

备注

鼠标和游戏板右操纵杆的输入也有单独处理的 ID。Input from the mouse and right thumbstick of a gamepad also have IDs that are handled separately.

在将指针事件映射到特定的游戏操作之后,可以更新 MoveLookController 对象与主游戏循环共享的数据。After the pointer events have been mapped to a specific game action, it's time to update the data the MoveLookController object shares with the main game loop.

在调用时,示例游戏中的 Update 方法将处理输入并更新速度和外观方向变量 (m _ 速度m _ lookdirection) ,然后游戏循环通过调用公共 速度lookdirection 方法来检索这些变量。When called, the Update method in the sample game processes the input and updates the velocity and look direction variables (m_velocity and m_lookdirection), which the game loop then retrieves by calling the public Velocity and LookDirection methods.

备注

有关 Update 方法的更多详细信息,可以在此页的后面部分看到。More details about the Update method can be seen later on this page.

游戏循环可以通过在 MoveLookController 实例上调用 IsFiring 方法测试玩家是否正在射击。The game loop can test to see if the player is firing by calling the IsFiring method on the MoveLookController instance. MoveLookController 通过检查了解玩家是否已在三种输入类型之一上按下射击按钮。The MoveLookController checks to see if the player has pressed the fire button on one of the three input types.

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;
}

现在,让我们更详细地了解这三个控件类型的实现。Now let's look at the implementation of each of the three control types in a little more detail.

添加相对鼠标控件Adding relative mouse controls

如果检测到鼠标移动,我们希望利用该移动确定相机的新的俯仰和偏航。If mouse movement is detected, we want to use that movement to determine the new pitch and yaw of the camera. 我们通过实现相对鼠标控件实现此目的,在该控件中我们处理鼠标所移动的相对距离(移动开始到停止之间的增量),与记录运动的绝对 x-y 像素坐标相反。We do that by implementing relative mouse controls, where we handle the relative distance the mouse has moved—the delta between the start of the movement and the stop—as opposed to recording the absolute x-y pixel coordinates of the motion.

为此,我们通过检查由 MouseMoved 事件返回的 Windows::Device::Input::MouseEventArgs::MouseDelta 参数对象上的 MouseDelta::XMouseDelta::Y 字段获取 X(水平运动)和 Y(垂直运动)坐标的变化。To do that, we obtain the changes in the X (the horizontal motion) and the Y (the vertical motion) coordinates by examining the MouseDelta::X and MouseDelta::Y fields on the Windows::Device::Input::MouseEventArgs::MouseDelta argument object returned by the MouseMoved event.

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;
    }
}

添加触控支持Adding touch support

触摸控件非常适合用于支持使用平板电脑的用户。Touch controls are great for supporting users with tablets. 此游戏通过划分屏幕的某些区域,利用每次针对游戏内特定操作的调整来收集触控输入。This game gathers touch input by zoning off certain areas of the screen with each aligning to specific in-game actions. 此游戏的触控输入使用三个区域。This game's touch input uses three zones.

移动观看触控布局

以下命令汇总了我们的触摸控件行为。The following commands summarize our touch control behavior. 用户输入User input | 操作Action :------- | :-------- 移动矩形Move rectangle | 触控输入转换为虚拟游戏杆,这时,垂直运动将转换为向前/向后位置移动,水平运动将转换为向左/向右位置移动。Touch input is converted into a virtual joystick where the vertical motion will be translated into forward/backward position motion and horizontal motion will be translated into left/right position motion. 射击矩形Fire rectangle | 射击视区。Fire a sphere. 触摸移动和射击矩形的外部Touch outside of move and fire rectangle | 更改相机视图的旋转(俯仰和偏航)。Change the rotation (the pitch and yaw) of the camera view.

MoveLookController 检查指针 ID 以确定发生事件的位置,并采取以下操作之一:The MoveLookController checks the pointer ID to determine where the event occurred, and takes one of the following actions:

  • 如果在移动或射击矩形中发生了 PointerMoved 事件,则更新控制器的指针位置。If the PointerMoved event occurred in the move or fire rectangle, update the pointer position for the controller.
  • 如果在屏幕的其他位置(定义为观看控制区)发生了 PointerMoved 事件,则计算观看方向矢量的俯仰和偏航变化。If the PointerMoved event occurred somewhere in the rest of the screen (defined as the look controls), calculate the change in pitch and yaw of the look direction vector.

在实现触摸控件之后,我们之前使用 Direct2D 绘制的矩形将指示玩家移动、射击和观看区域在哪里。Once we've implemented our touch controls, the rectangles we drew earlier using Direct2D will indicate to players where the move, fire, and look zones are.

触摸控件

现在我们来看看我们如何实现每个控件。Now let's take a look at how we implement each control.

移动和射击控制器Move and fire controller

屏幕左下区的移动控制器矩形用作方向板。The move controller rectangle in the lower left quadrant of the screen is used as a directional pad. 在此空间内左右滑动拇指可左右移动玩家,而上下滑动可前后移动相机。Sliding your thumb left and right within this space moves the player left and right, while up and down moves the camera forward and backward. 完成此设置后,点击屏幕右下区的射击控制器可射击视区。After setting this up, tapping the fire controller in the lower right quadrant of the screen fires a sphere.

SetMoveRectSetFireRect 方法创建输入矩形,使用两个 2D 矢量在屏幕上指定每个矩形的左上角和右下角位置。The SetMoveRect and SetFireRect methods create our input rectangles, taking two, 2D vectors to specify each rectangles' upper left and lower right corner positions on the screen.

然后,将参数分配给 m _ fireUpperLeftm _ fireLowerRight ,这将帮助我们确定用户是否在矩形内触摸。The parameters are then assigned to m_fireUpperLeft and m_fireLowerRight that will help us determine if the user is touching within on of the rectangles.

m_fireUpperLeft = upperLeft;
m_fireLowerRight = lowerRight;

如果重新调整屏幕大小,这些矩形将重新绘制为相应的大小。If the screen is resized, these rectangles are redrawn to the approperiate size.

现在,我们已经对控件进行了分区,应该确定用户何时可以实际使用它们了。Now that we've zoned off our controls, it's time to determine when a user is actually using them. 为此,我们在 MoveLookController::InitWindow 方法中为用户何时按下、移动或释放指针设置了一些事件处理程序。To do this, we set up some event handlers in the MoveLookController::InitWindow method for when the user presses, moves, or releases their pointer.

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

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

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

我们将首先确定,用户在使用 OnPointerPressed 方法第一次按移动或射击矩形内部时将发生什么。We'll first determine what happens when the user first presses within the move or fire rectangles using the OnPointerPressed method. 在这里,我们检查他们触摸控件的位置以及指针是否已在该控制器中。Here we check where they're touching a control and if a pointer is already in that controller. 如果这是触摸特定控件的第一个手指,我们执行以下操作。If this is the first finger to touch the specific control, we do the following.

  • 将 system.windows.uielement.touchdown> 的位置以 m _ moveFirstDownm _ fireFirstDown 存储为2d 向量。Store the location of the touchdown in m_moveFirstDown or m_fireFirstDown as a 2D vector.
  • 将指针 ID 分配给 m _ movePointerIDm _ firePointerIDAssign the pointer ID to m_movePointerID or m_firePointerID.
  • 将正确的 正在使用 标志设置 (m _ moveInUsem _ fireInUse) , true 因为现在我们有该控件的活动指针。Set the proper InUse flag (m_moveInUse or m_fireInUse) to true since we now have an active pointer for that control.
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;
                ...
            }
        }
        ...

现在,我们已经确定了用户是在触摸移动控件还是射击控件,我们会看到玩家是否使用他们按下的手指进行了任何移动。Now that we've determined whether the user is touching a move or fire control, we see if the player is making any movements with their pressed finger. 使用 MoveLookController::OnPointerMoved 方法,我们检查哪个指针已移动,然后将它的新位置存储为 2D 矢量。Using the MoveLookController::OnPointerMoved method, we check what pointer has moved and then store its new position as a 2D vector.

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;
    }
    ...

当用户在控件内创建了手势后,他们将释放指针。Once the user has made their gestures within the controls, they'll release the pointer. 使用 MoveLookController::OnPointerReleased 方法,我们确定释放了哪个指针并执行一系列重置。Using the MoveLookController::OnPointerReleased method, we determine which pointer has been released and do a series of resets.

如果移动控件已释放,我们执行以下操作。If the move control has been released, we do the following.

  • 在所有方向将玩家的速度设置为 0 以阻止他们在游戏中移动。Set the velocity of the player to 0 in all directions to prevent them from moving in the game.
  • m _ moveInUse 切换为, false 因为用户不再触及移动控制器。Switch m_moveInUse to false since the user is no longer touching the move controller.
  • 将移动指针 ID 设置为 0,因为移动控制器中不再有指针。Set the move pointer ID to 0 since there's no longer a pointer in the move controller.
if (pointerID == m_movePointerID)
{
    // Stop on release.
    m_velocity = XMFLOAT3(0, 0, 0);
    m_moveInUse = false;
    m_movePointerID = 0;
}

对于射击控件,如果它已被释放,我们要做的只是将 m_fireInUse 标志切换为 false,并将射击指针 ID 切换为 0,因为射击控件内不会再有指针。For the fire control, if it has been released all we do is switch the m_fireInUse flag to false and the fire pointer ID to 0 since there's no longer a pointer in the fire control.

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

观看控制器Look controller

我们将屏幕未使用区域的触摸设备指针事件视为观看控制器。We treat touch device pointer events for the unused regions of the screen as the look controller. 在区域四处滑动手指可更改玩家相机的俯仰和偏航(旋转)。Sliding your finger around this zone changes the pitch and yaw (rotation) of the player camera.

如果在此区域的触摸设备上引发了 MoveLookController::OnPointerPressed 事件,并且游戏状态设置为 Active,则为其分配一个指针 ID。If the MoveLookController::OnPointerPressed event is raised on a touch device in this region and the game state is set to Active, it's assigned a pointer ID.

// 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 将触发该事件的指针的指针 ID 分配到对应于观看区域的特定变量。Here the MoveLookController assigns the pointer ID for the pointer that fired the event to a specific variable that corresponds to the look region. 如果在外观区域发生触摸,则 m _ lookPointerID 变量设置为触发事件的指针 ID。In the case of a touch occurring in the look region, the m_lookPointerID variable is set to the pointer ID that fired the event. 布尔变量 m _ lookInUse也设置为指示该控件尚未释放。A boolean variable, m_lookInUse, is also set to indicate that the control has not yet been released.

现在,让我们看看示例游戏如何处理 PointerMoved 触摸屏事件。Now, let's look at how the sample game handles the PointerMoved touch screen event.

MoveLookController::OnPointerMoved 方法内,我们查看哪类指针 ID 被分配到该事件。Within the MoveLookController::OnPointerMoved method, we check to see what kind of pointer ID has been assigned to the event. 如果是 m_lookPointerID,我们将计算指针位置的变化。If it's m_lookPointerID, we calculate the change in position of the pointer. 然后,我们使用此增量计算应更改多少旋转。We then use this delta to calculate how much the rotation should change. 最后,我们可以更新要在游戏中使用的 m _ 跨度m _ 偏航 来更改播放机旋转。Finally we're at a point where we can update the m_pitch and m_yaw to be used in the game to change the player rotation.

// 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 触摸屏事件。The last piece we'll look at is how the sample game handles the PointerReleased touch screen event. 当用户完成了触摸手势并在屏幕上删除了手指后,MoveLookController::OnPointerReleased 将启动。Once the user has finished the touch gesture and removed their finger from the screen, MoveLookController::OnPointerReleased is initiated. 如果触发 PointerReleased 事件的指针的指针 ID 是以前记录的移动指针的 ID,则 MoveLookController 将速度设置为 0,这是因为玩家已停止触摸观看区域。If the ID of the pointer that fired the PointerReleased event is the ID of the previously recorded move pointer, the MoveLookController sets the velocity to 0 because the player has stopped touching the look area.

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

添加鼠标和键盘支持Adding mouse and keyboard support

此游戏具有以下键盘和鼠标控件布局。This game has the following control layout for keyboard and mouse.

用户输入User input 操作Action
WW 向前移动玩家Move player forward
AA 向左移动玩家Move player left
SS 向后移动玩家Move player backward
DD 向右移动玩家Move player right
XX 向上移动视图Move view up
空格键Space bar 向上移动视图Move view down
PP 暂停游戏Pause the game
鼠标移动Mouse movement 更改相机视图的旋转(俯仰和偏航)Change the rotation (the pitch and yaw) of the camera view
鼠标左键Left mouse button 射击视区Fire a sphere

为了使用键盘,示例游戏在MoveLookController:: InitWindow方法中注册了两个新的事件: CoreWindow:: KeyUpCoreWindow:: KeyDownTo use the keyboard, the sample game registers two new events, CoreWindow::KeyUp and CoreWindow::KeyDown, within the MoveLookController::InitWindow method. 这些事件处理键的按下和释放。These events handle the press and release of a key.

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

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

尽管鼠标使用的也是指针,但它的处理方式与触摸控件稍有不同。The mouse is treated a little differently from the touch controls even though it uses a pointer. 若要与我们的控件布局保持一致,无论何时移动鼠标,MoveLookController 都会旋转,并在按下鼠标左键时射击。To align with our control layout, the MoveLookController rotates the camera whenever the mouse is moved, and fires when the left mouse button is pressed.

这在 MoveLookControllerOnPointerPressed 方法中处理。This is handled in the OnPointerPressed method of the MoveLookController.

在此方法中,我们将检查是否有与该枚举一起使用的指针设备类型 Windows::Devices::Input::PointerDeviceTypeIn this method we check to see what type of pointer device is being used with the Windows::Devices::Input::PointerDeviceType enum. 如果游戏处于 Active 状态且 PointerDeviceType 不是 Touch,我们假设为鼠标输入。If the game is Active and the PointerDeviceType isn't Touch, we assume it's mouse input.

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 方法,输入完成。When the player stops pressing one of the mouse buttons, the CoreWindow::PointerReleased mouse event is raised, calling the MoveLookController::OnPointerReleased method, and the input is complete. 此时,如果按下了鼠标左键并在现在释放,球体将停止射击。At this point, spheres will stop firing if the left mouse button was being pressed and is now released. 由于始终启用观看,所以游戏将继续使用同一鼠标指针跟踪正在进行的观看事件。Because look is always enabled, the game continues to use the same mouse pointer to track the ongoing look events.

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;

现在,我们来看看要支持的最后一种控件类型:游戏板。Now let's look at the last control type we'll be supporting: gamepads. 游戏板独立于触摸控件和鼠标控件进行处理,因为它们不使用指针对象。Gamepads are handled separately from the touch and mouse controls since they doesn't use the pointer object. 出于此原因,需要添加几个新的事件处理程序和方法。Because of this, a few new event handlers and methods will need to be added.

添加游戏板支持Adding gamepad support

对于此游戏,游戏板支持通过调用 Windows.Gaming.Input API 添加。For this game, gamepad support is added by calls to the Windows.Gaming.Input APIs. 这组 API 提供对游戏控制器输入(如赛车方向盘和飞行杆)的访问。This set of APIs provides access to game controller inputs like racing wheels and flight sticks.

下面将是我们的游戏板控件。The following will be our gamepad controls.

用户输入User input 操作Action
左摇杆Left analog stick 移动玩家Move player
右摇杆Right analog stick 更改相机视图的旋转(俯仰和偏航)Change the rotation (the pitch and yaw) of the camera view
右扳机键Right trigger 射击视区Fire a sphere
“开始”/“菜单”按钮Start/Menu button 暂停或继续游戏Pause or resume the game

InitWindow 方法中,我们添加了两个新事件来确定游戏板是否已添加删除In the InitWindow method, we add two new events to determine if a gamepad has been added or removed. 这些事件更新 m_gamepadsChanged 属性。These events update the m_gamepadsChanged property. UpdatePollingDevices 方法中使用此方法来检查已知 gamepads 的列表是否已更改。This is used in the UpdatePollingDevices method to check if the list of known gamepads has changed.

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

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

备注

当应用未处于焦点时,UWP 应用无法从 Xbox One 控制器接收输入。UWP apps cannot receive input from an Xbox One Controller while the app is not in focus.

UpdatePollingDevices 方法The UpdatePollingDevices method

MoveLookController 实例的 UpdatePollingDevices 方法立即检查游戏板是否已连接。The UpdatePollingDevices method of the MoveLookController instance immediately checks to see if a gamepad is connected. 如果已连接,我们将开始通过 Gamepad.GetCurrentReading 读取它的状态。If one is, we'll start reading its state with Gamepad.GetCurrentReading. 这将返回 GamepadReading 结构,让我们可以检查点击了哪些按钮或移动了什么操纵杆。This returns the GamepadReading struct, allowing us to check what buttons have been clicked or thumbsticks moved.

如果游戏的状态是 WaitForInput,我们将仅侦听控制器的“开始”/“菜单”按钮,以便可以继续游戏。If the state of the game is WaitForInput, we only listen for the Start/Menu button of the controller so that the game can be resumed.

如果是 Active 状态,我们将检查用户的输入并确定需要发生哪些游戏内操作。If it's Active, we check the user's input and determine what in-game action needs to happen. 例如,如果用户向特定方向移动了左摇杆,这让游戏知道我们需要朝摇杆移动的方法移动玩家。For instance, if the user moved the left analog stick in a specific direction, this lets the game know we need to move the player in the direction the stick is being moved. 向特定方向移动摇杆必须表现为大于死区的半径;否则,不会发生任何操作。The movement of the stick in a specific direction must register as larger than the radius of the dead zone; otherwise, nothing will happen. 此死区半径需要阻止“漂移”,当控制器从玩家放在摇杆的拇指上拾取较小移动时,将呈现“漂移”。This dead zone radius is necessary to prevent "drifting," which is when the controller picks up small movements from the player's thumb as it rests on the stick. 如果没有死区,控制对用户可能会表现得过于敏感。Without dead zones, the controls can appear too sensitive to the user.

x 轴和 y 轴的操纵杆输入都在 -1 和 1 之间。Thumbstick input is between -1 and 1 for both the x and y axis. 以下常量指定操纵杆死区的半径。The following consant specifies the radius of the thumbstick dead zone.

#define THUMBSTICK_DEADZONE 0.25f

使用此变量,我们随后将开始处理可操作的操纵杆输入。Using this variable, we'll then begin processing actionable thumbstick input. 移动将在任一轴上按照 [-1, -.26] 或 [.26, 1] 值进行。Movement would occur with a value from [-1, -.26] or [.26, 1] on either axis.

操纵杆的死区

这个 UpdatePollingDevices 方法处理左右操纵杆。This piece of the UpdatePollingDevices method handles the left and right thumbsticks. 检查每个杆的 X 和 Y 值以查看它们是否在死区之外。Each stick's X and Y values are checked to see if they are outside of the dead zone. 如果有一个或两个值都在死区之外,我们将更新相应的组件。If one or both are, we'll update the corresponding component. 例如,如果左操纵杆沿 X 轴向左移动,我们会向 m_moveCommand 矢量的 x 组件添加 -1。For example, if the left thumbstick is being moved left along the X axis, we'll add -1 to the x component of the m_moveCommand vector. 此矢量将用于聚合跨所有设备的所有移动,随后还将用于计算玩家应移动的位置。This vector is what will be used to aggregate all movements across all devices and will later be used to calculate where the player should move.

// 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;
}

与左操纵杆控制移动的方式类似,右操纵杆控制相机的旋转。Similar to how the left stick controls movement, the right stick controls the rotation of the camera.

右操纵杆的行为与鼠标和键盘控件设置中的鼠标移动行为一致。The right thumb stick behavior aligns with the behavior of mouse movement in our mouse and keyboard control setup. 如果杆在死区之外,我们将计算当前指针位置与用户正在尝试查看的位置之间的差值。If the stick is outside of the dead zone, we calculate the difference between the current pointer position and where the user is now trying to look. 这一指针位置的更改 (pointerDelta) 随后用于更新相机旋转的俯仰和偏航(随后应用于 Update 方法)。This change in pointer position (pointerDelta) is then used to update the pitch and yaw of the camera rotation that later get applied in our Update method. pointerDelta 矢量可能看起来很熟悉,因为它也会在 MoveLookController::OnPointerMoved 方法中用来跟踪鼠标和触摸输入的指针位置的变化。The pointerDelta vector may look familiar because it's also used in the MoveLookController::OnPointerMoved method to keep track of change in pointer position for our mouse and touch inputs.

// 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);

如果没有射击视区功能,游戏的控件是不完整的!The game's controls wouldn't be complete without the ability to fire spheres!

UpdatePollingDevices 方法还会检查是否按下了正确的触发器。This UpdatePollingDevices method also checks if the right trigger is being pressed. 如果正确,我们的 m_firePressed 属性将翻转为 true,通知游戏应开始射击视区。If it is, our m_firePressed property is flipped to true, signaling to the game that spheres should start firing.

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

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

Update 方法The Update method

最后作个收尾,我们来深入了解一下 Update 方法。To wrap things up, let's dig deeper into the Update method. 此方法合并玩家使用任何受支持的输入执行的任何移动或旋转,以生成速度矢量并更新用于访问游戏循环的俯仰和偏航值。This method merges any movements or rotations that the player made with any supported input to generate a velocity vector and update our pitch and yaw values for our game loop to access.

Update 方法通过调用 UpdatePollingDevices 启动,以更新控制器的状态。The Update method kicks things off by calling UpdatePollingDevices to update the state of the controller. 此方法还收集游戏板的所有输入,并将其移动添加到 m_moveCommand 矢量。This method also gathers any input from a gamepad and adds its movements to the m_moveCommand vector.

在我们的 Update 方法中,我们随后执行了以下输入检查。In our Update method we then perform the following input checks.

  • 如果玩家使用移动控制器矩形,我们随后将确定指针位置的变化,并使用此信息计算用户是否将指针移出了控制器的死区。If the player is using the move controller rectangle, we'll then determine the change in pointer position and use that to calculate if the user has moved the pointer out of the controller's dead zone. 如果移出,m_moveCommand 矢量属性随后将更新为虚拟游戏杆值。If outside of the dead zone, the m_moveCommand vector property is then updated with the virtual joystick value.
  • 如果按下任何移动键盘输入,则 1.0f -1.0f 会在m_moveCommand向量的相应分量中添加或的值,以便 — 1.0f 向前和 -1.0f 向后。If any of the movement keyboard inputs are pressed, a value of 1.0f or -1.0f are added in the corresponding component of the m_moveCommand vector—1.0f for forward, and -1.0f for backward.

考虑了所有移动输入后,我们通过一些计算来运行 m_moveCommand 矢量以生成新矢量,用于表示与游戏世界有关的玩家方向。Once all movement input has been taken into account, we then run the m_moveCommand vector through some calculations to generate a new vector that represents the direction of the player with regards to the game world. 然后,我们执行与该世界有关的移动,并随着这个方向的速度对玩家应用这些移动。We then take our movements in relation to the world and apply them to the player as velocity in that direction. 最后,我们将 m_moveCommand 矢量重置为 (0.0f, 0.0f, 0.0f) 以为下一个游戏帧作好一切准备。Finally we reset the m_moveCommand vector to (0.0f, 0.0f, 0.0f) so that everything is ready for the next game frame.

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);
}

后续步骤Next steps

现在,我们已经添加了控件,我们还需要添加另一项功能来创建沉浸式游戏:声音!Now that we have added our controls, there's another feature we need to add to create an immersive game: sound! 对于任何游戏而言,音乐和声音效果都非常重要,下面我们讨论添加声音Music and sound effects are important to any game, so let's discuss adding sound next.