游戏的移动查找控件
了解如何将传统的鼠标和键盘移动外观控件(也称为鼠标浏览控件)添加到 DirectX 游戏。
我们还讨论了对触摸设备的移动外观支持,移动控制器定义为屏幕左下部分,其行为类似于方向输入,以及为屏幕的其余部分定义的外观控制器,相机位于该区域中最后一个位置。
如果这是一个不熟悉的控制概念,请这样思考:键盘(或基于触摸的定向输入框)控制这三维空间中的腿,行为就像你的腿只能够向前或向后移动,或者向左和向右分层。 鼠标(或触摸指针)控制头部。 使用头部向左、向右、向上或向下或在该平面中的某个位置查找。 如果你的视图中有一个目标,则使用鼠标将相机视图居中,然后按前进键向目标移动,或返回移动。 若要对目标进行圆圈,请将相机视图居中定位在目标上,同时向左或向右移动。 你可以看到这是一种非常有效的控制方法来导航 3D 环境!
这些控件通常称为游戏中的 WASD 控件,其中 W、A、S 和 D 键用于 x-z 平面固定相机移动,鼠标用于控制相机围绕 x 轴和 y 轴旋转。
目标
- 为鼠标和键盘以及触摸屏将基本移动外观控件添加到 DirectX 游戏。
- 实现用于导航 3D 环境的第一人称相机。
有关触摸控件实现的说明
对于触摸控件,我们实现两个控制器:移动控制器,它处理 x-z 平面相对于相机外观点的移动;和外观控制器,旨在相机的视点。 移动控制器映射到键盘 WASD 按钮,外观控制器映射到鼠标。 但对于触摸控件,我们需要定义用作方向输入或虚拟 WASD 按钮的屏幕区域,其余屏幕用作外观控件的输入空间。
我们的屏幕如下所示。
当你在屏幕左下角移动触摸指针(而不是鼠标!),任何向上移动都会使相机向前移动。 任何向下移动都会使相机向后移动。 移动控制器指针空间内的左右移动也是如此。 在该空间之外,它将成为一个外观控制器 - 你只需触摸或拖动相机到你想要它面对的位置。
设置基本输入事件基础结构
首先,我们必须创建控件类,用于处理鼠标和键盘中的输入事件,并基于该输入更新相机透视。 由于我们正在实现移动外观控件,因此将其称为 MoveLookController。
using namespace Windows::UI::Core;
using namespace Windows::System;
using namespace Windows::Foundation;
using namespace Windows::Devices::Input;
#include <DirectXMath.h>
// Methods to get input from the UI pointers
ref class MoveLookController
{
}; // class MoveLookController
现在,让我们创建一个标头,用于定义移动外观控制器及其第一人称相机的状态,以及实现控件和更新相机状态的基本方法和事件处理程序。
#define ROTATION_GAIN 0.004f // Sensitivity adjustment for the look controller
#define MOVEMENT_GAIN 0.1f // Sensitivity adjustment for the move controller
ref class MoveLookController
{
private:
// Properties of the controller object
DirectX::XMFLOAT3 m_position; // The position of the controller
float m_pitch, m_yaw; // Orientation euler angles in radians
// Properties of the Move control
bool m_moveInUse; // Specifies whether the move control is in use
uint32 m_movePointerID; // Id of the pointer in this control
DirectX::XMFLOAT2 m_moveFirstDown; // Point where initial contact occurred
DirectX::XMFLOAT2 m_movePointerPosition; // Point where the move pointer is currently located
DirectX::XMFLOAT3 m_moveCommand; // The net command from the move control
// Properties of the Look control
bool m_lookInUse; // Specifies whether the look control is in use
uint32 m_lookPointerID; // Id of the pointer in this control
DirectX::XMFLOAT2 m_lookLastPoint; // Last point (from last frame)
DirectX::XMFLOAT2 m_lookLastDelta; // For smoothing
bool m_forward, m_back; // States for movement
bool m_left, m_right;
bool m_up, m_down;
public:
// Methods to get input from the UI pointers
void OnPointerPressed(
_In_ Windows::UI::Core::CoreWindow^ sender,
_In_ Windows::UI::Core::PointerEventArgs^ args
);
void OnPointerMoved(
_In_ Windows::UI::Core::CoreWindow^ sender,
_In_ Windows::UI::Core::PointerEventArgs^ args
);
void OnPointerReleased(
_In_ Windows::UI::Core::CoreWindow^ sender,
_In_ Windows::UI::Core::PointerEventArgs^ args
);
void OnKeyDown(
_In_ Windows::UI::Core::CoreWindow^ sender,
_In_ Windows::UI::Core::KeyEventArgs^ args
);
void OnKeyUp(
_In_ Windows::UI::Core::CoreWindow^ sender,
_In_ Windows::UI::Core::KeyEventArgs^ args
);
// Set up the Controls that this controller supports
void Initialize( _In_ Windows::UI::Core::CoreWindow^ window );
void Update( Windows::UI::Core::CoreWindow ^window );
internal:
// Accessor to set position of controller
void SetPosition( _In_ DirectX::XMFLOAT3 pos );
// Accessor to set position of controller
void SetOrientation( _In_ float pitch, _In_ float yaw );
// Returns the position of the controller object
DirectX::XMFLOAT3 get_Position();
// Returns the point which the controller is facing
DirectX::XMFLOAT3 get_LookPoint();
}; // class MoveLookController
我们的代码包含 4 组私有字段。 让我们回顾一下每个用途。
首先,我们定义一些有用的字段,用于保存有关相机视图的更新信息。
- m_position 是 3D 场景(使用场景坐标)中相机的位置(因此是一个视觉平面)。
- m_pitch 是相机的俯仰度或其绕视觉平面的 x 轴上下旋转弧度。
- m_yaw 是相机的偏航度或其绕视觉平面的 y 轴左右旋转弧度。
现在,让我们定义用于存储有关控制器状态和位置的信息的字段。 首先,我们将定义基于触摸的移动控制器所需的字段。 (移动控制器的键盘实现没有任何特殊需要。我们只是使用特定处理程序读取键盘事件。
- m_moveInUse 指示是否正在使用移动控制器。
- m_movePointerID 是当前移动指针的唯一 ID。 在检查指针 ID 值时,我们使用它区分外观指针和移动指针。
- m_moveFirstDown 是玩家在屏幕上首次触摸移动控制器指针区域的点。 我们稍后使用此值来设置死区,以保持微小的移动免受视图的抖动。
- m_movePointerPosition 是玩家当前在屏幕上将指针移动到的点。 我们通过检查它相对于 m_moveFirstDown 的移动,确定玩家要移动的方向。
- m_moveCommand 是移动控制器的最终计算命令:向上(前进)、向下(后退)、向左或向右。
现在,我们定义用于外观控制器的字段,包括鼠标和触摸实现。
- m_lookInUse 指示是否正在使用观看控件。
- m_lookPointerID 是当前观看控制器指针的唯一 ID。 在检查指针 ID 值时,我们使用它区分外观指针和移动指针。
- m_lookLastPoint 是场景坐标中从上一帧中捕获的最后一个点。
- m_lookLastDelta 是在当前 m_position 与 m_lookLastPoint 之间计算出的差异。
最后,为 6 度移动定义 6 个布尔值,用于指示每个方向移动操作的当前状态(打开或关闭):
- m_forward、m_back、m_left、m_right、m_up和 m_down。
我们使用 6 个事件处理程序捕获用于更新控制器状态的输入数据:
- OnPointerPressed。 玩家使用游戏屏幕中的指针按下鼠标左键,或触摸屏幕。
- OnPointerMoved。 玩家在游戏屏幕中使用指针移动鼠标,或在屏幕上拖动触摸指针。
- OnPointerReleased。 玩家在游戏屏幕中使用指针释放了鼠标左键,或停止触摸屏幕。
- OnKeyDown。 玩家按下了一个键。
- OnKeyUp。 玩家释放了一个密钥。
最后,我们使用这些方法和属性来初始化、访问和更新控制器的状态信息。
- Initialize。 我们的应用调用此事件处理程序来初始化控件并将其附加到描述显示窗口的 CoreWindow 对象。
- SetPosition。 我们的应用调用此方法来设置场景空间中控件的 (x、y 和 z) 坐标。
- SetOrientation。 我们的应用调用此方法来设置相机的俯仰和偏航。
- get_Position。 我们的应用访问此属性以获取相机在场景空间中的当前位置。 使用此属性作为将当前相机位置传达给应用的方法。
- get_LookPoint。 我们的应用访问此属性以获取控制器相机面临的当前点。
- 更新。 读取移动和观看控制器的状态,并更新相机位置。 从应用的主循环中不断调用此方法,以刷新相机控制器数据和场景空间中的相机位置。
现在,你已拥有实现移动外观控件所需的所有组件。 因此,让我们将这些部分连接在一起。
创建基本输入事件
Windows 运行时事件调度程序提供 5 个事件,我们希望 MoveLookController 类的实例处理:
这些事件在 CoreWindow 类型上实现。 我们假设你有一个 CoreWindow 对象要处理。 如果不知道如何获取,请参阅如何设置通用 Windows 平台(UWP)C++应用以显示 DirectX 视图。
当这些事件在应用运行时触发时,处理程序将更新在专用字段中定义的控制器的状态信息。
首先,让我们填充鼠标和触摸指针事件处理程序。 在第一个事件处理程序 OnPointerPressed()中,我们从 CoreWindow 获取指针的 x-y 坐标,当用户单击鼠标或触摸外观控制器区域中的屏幕时管理显示。
OnPointerPressed
void MoveLookController::OnPointerPressed(
_In_ CoreWindow^ sender,
_In_ PointerEventArgs^ args)
{
// Get the current pointer position.
uint32 pointerID = args->CurrentPoint->PointerId;
DirectX::XMFLOAT2 position = DirectX::XMFLOAT2( args->CurrentPoint->Position.X, args->CurrentPoint->Position.Y );
auto device = args->CurrentPoint->PointerDevice;
auto deviceType = device->PointerDeviceType;
if ( deviceType == PointerDeviceType::Mouse )
{
// Action, Jump, or Fire
}
// Check if this pointer is in the move control.
// Change the values to percentages of the preferred screen resolution.
// You can set the x value to <preferred resolution> * <percentage of width>
// for example, ( position.x < (screenResolution.x * 0.15) ).
if (( position.x < 300 && position.y > 380 ) && ( deviceType != PointerDeviceType::Mouse ))
{
if ( !m_moveInUse ) // if no pointer is in this control yet
{
// Process a DPad touch down event.
m_moveFirstDown = position; // Save the location of the initial contact.
m_movePointerPosition = position;
m_movePointerID = pointerID; // Store the id of the pointer using this control.
m_moveInUse = TRUE;
}
}
else // This pointer must be in the look control.
{
if ( !m_lookInUse ) // If no pointer is in this control yet...
{
m_lookLastPoint = position; // save the point for later move
m_lookPointerID = args->CurrentPoint->PointerId; // store the id of pointer using this control
m_lookLastDelta.x = m_lookLastDelta.y = 0; // these are for smoothing
m_lookInUse = TRUE;
}
}
}
此事件处理程序检查指针是否不是鼠标(出于此示例的目的,该示例同时支持鼠标和触摸),以及它是否位于移动控制器区域中。 如果满足这两个条件,它将检查刚才是否按下了指针,具体地说,通过测试 m_moveInUse 是否为 false 检查此单击是否与上次移动或观看输入无关。 如果无关,处理程序将捕获发生点按操作的移动控制器区域中的点,并将 m_moveInUse 设置为 true,以便当再次调用此处理程序时,它不会覆盖移动控制器输入交互的起始位置。 它还会将移动控制器指针 ID 更新为当前指针的 ID。
如果指针是鼠标,或者触摸指针不在移动控制器区域中,则它必须位于外观控制器区域中。 该指针将 m_lookLastPoint 设置为用户按下鼠标按钮或触摸并点按的当前位置、重置增量,并将观看控制器的指针 ID 更新为当前指针 ID。 它还将外观控制器的状态设置为活动状态。
OnPointerMoved
void MoveLookController::OnPointerMoved(
_In_ CoreWindow ^sender,
_In_ PointerEventArgs ^args)
{
uint32 pointerID = args->CurrentPoint->PointerId;
DirectX::XMFLOAT2 position = DirectX::XMFLOAT2(args->CurrentPoint->Position.X, args->CurrentPoint->Position.Y);
// Decide which control this pointer is operating.
if (pointerID == m_movePointerID) // This is the move pointer.
{
// Move control
m_movePointerPosition = position; // Save the current position.
}
else if (pointerID == m_lookPointerID) // This is the look pointer.
{
// Look control
DirectX::XMFLOAT2 pointerDelta;
pointerDelta.x = position.x - m_lookLastPoint.x; // How far did pointer move
pointerDelta.y = position.y - m_lookLastPoint.y;
DirectX::XMFLOAT2 rotationDelta;
rotationDelta.x = pointerDelta.x * ROTATION_GAIN; // Scale for control sensitivity.
rotationDelta.y = pointerDelta.y * ROTATION_GAIN;
m_lookLastPoint = position; // Save for the next time through.
// Update our orientation based on the command.
m_pitch -= rotationDelta.y; // Mouse y increases down, but pitch increases up.
m_yaw -= rotationDelta.x; // Yaw is defined as CCW around the y-axis.
// Limit the pitch to straight up or straight down.
m_pitch = (float)__max(-DirectX::XM_PI / 2.0f, m_pitch);
m_pitch = (float)__min(+DirectX::XM_PI / 2.0f, m_pitch);
}
}
每当指针移动时,OnPointerMoved 事件处理程序就会触发(在这种情况下,如果拖动触摸屏指针,或在按下左按钮时移动鼠标指针)。 如果指针 ID 与移动控制器指针的 ID 相同,则为移动指针;否则,我们检查它是否为活动指针的外观控制器。
如果是移动控制器,我们只需更新指针位置。 只要 PointerMoved 事件不断激发,我们不断更新它,因为我们希望将最终位置与使用 OnPointerPressed 事件处理程序捕获的第一个位置进行比较。
如果是外观控制器,事情会稍微复杂一些。 我们需要计算新的外观点,并将相机居中,因此我们计算最后一个看点与当前屏幕位置之间的增量,然后我们相乘与比例因子,我们可以进行调整,使外观移动相对于屏幕移动的距离更小或更大。 使用该值,我们计算音调和偏航。
最后,当玩家停止移动鼠标或触摸屏幕时,我们需要停用移动或观看控制器行为。 我们使用在触发 PointerReleased 时调用的 OnPointerReleased 将 m_moveInUse 或 m_lookInUse 设置为 FALSE,并关闭相机平移,将指针 ID 设置为零。
OnPointerReleased
void MoveLookController::OnPointerReleased(
_In_ CoreWindow ^sender,
_In_ PointerEventArgs ^args)
{
uint32 pointerID = args->CurrentPoint->PointerId;
DirectX::XMFLOAT2 position = DirectX::XMFLOAT2( args->CurrentPoint->Position.X, args->CurrentPoint->Position.Y );
if ( pointerID == m_movePointerID ) // This was the move pointer.
{
m_moveInUse = FALSE;
m_movePointerID = 0;
}
else if (pointerID == m_lookPointerID ) // This was the look pointer.
{
m_lookInUse = FALSE;
m_lookPointerID = 0;
}
}
到目前为止,我们处理了所有触摸屏事件。 现在,让我们处理基于键盘的移动控制器的键输入事件。
OnKeyDown
void MoveLookController::OnKeyDown(
__in CoreWindow^ sender,
__in KeyEventArgs^ args )
{
Windows::System::VirtualKey Key;
Key = args->VirtualKey;
// Figure out the command from the keyboard.
if ( Key == VirtualKey::W ) // Forward
m_forward = true;
if ( Key == VirtualKey::S ) // Back
m_back = true;
if ( Key == VirtualKey::A ) // Left
m_left = true;
if ( Key == VirtualKey::D ) // Right
m_right = true;
}
只要按下其中一个键,此事件处理程序就会将相应的方向移动状态设置为 true。
OnKeyUp
void MoveLookController::OnKeyUp(
__in CoreWindow^ sender,
__in KeyEventArgs^ args)
{
Windows::System::VirtualKey Key;
Key = args->VirtualKey;
// Figure out the command from the keyboard.
if ( Key == VirtualKey::W ) // forward
m_forward = false;
if ( Key == VirtualKey::S ) // back
m_back = false;
if ( Key == VirtualKey::A ) // left
m_left = false;
if ( Key == VirtualKey::D ) // right
m_right = false;
}
当密钥释放时,此事件处理程序将其设置为 false。 调用 Update 时,它会检查这些方向移动状态,并相应地移动相机。 这比触摸实现简单一点!
初始化触摸控件和控制器状态
让我们立即连接事件,并初始化所有控制器状态字段。
初始化
void MoveLookController::Initialize( _In_ CoreWindow^ window )
{
// Opt in to receive touch/mouse events.
window->PointerPressed +=
ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &MoveLookController::OnPointerPressed);
window->PointerMoved +=
ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &MoveLookController::OnPointerMoved);
window->PointerReleased +=
ref new TypedEventHandler<CoreWindow^, PointerEventArgs^>(this, &MoveLookController::OnPointerReleased);
window->CharacterReceived +=
ref new TypedEventHandler<CoreWindow^, CharacterReceivedEventArgs^>(this, &MoveLookController::OnCharacterReceived);
window->KeyDown +=
ref new TypedEventHandler<CoreWindow^, KeyEventArgs^>(this, &MoveLookController::OnKeyDown);
window->KeyUp +=
ref new TypedEventHandler<CoreWindow^, KeyEventArgs^>(this, &MoveLookController::OnKeyUp);
// Initialize the state of the controller.
m_moveInUse = FALSE; // No pointer is in the Move control.
m_movePointerID = 0;
m_lookInUse = FALSE; // No pointer is in the Look control.
m_lookPointerID = 0;
// Need to init this as it is reset every frame.
m_moveCommand = DirectX::XMFLOAT3( 0.0f, 0.0f, 0.0f );
SetOrientation( 0, 0 ); // Look straight ahead when the app starts.
}
Initialize 将指向该应用的 CoreWindow 实例的引用作为参数提供,并在该 CoreWindow 上注册我们已开发的相应事件处理程序。 它初始化移动和查找指针的 ID,将触摸屏移动控制器实现的命令矢量设置为零,并在应用启动时直接设置相机。
获取和设置相机的位置和方向
让我们定义一些方法来获取和设置相机相对于视区的位置。
void MoveLookController::SetPosition( _In_ DirectX::XMFLOAT3 pos )
{
m_position = pos;
}
// Accessor to set the position of the controller.
void MoveLookController::SetOrientation( _In_ float pitch, _In_ float yaw )
{
m_pitch = pitch;
m_yaw = yaw;
}
// Returns the position of the controller object.
DirectX::XMFLOAT3 MoveLookController::get_Position()
{
return m_position;
}
// Returns the point at which the camera controller is facing.
DirectX::XMFLOAT3 MoveLookController::get_LookPoint()
{
float y = sinf(m_pitch); // Vertical
float r = cosf(m_pitch); // In the plane
float z = r*cosf(m_yaw); // Fwd-back
float x = r*sinf(m_yaw); // Left-right
DirectX::XMFLOAT3 result(x,y,z);
result.x += m_position.x;
result.y += m_position.y;
result.z += m_position.z;
// Return m_position + DirectX::XMFLOAT3(x, y, z);
return result;
}
更新控制器状态信息
现在,我们将执行计算,可将 m_movePointerPosition 中跟踪的指针坐标信息转换为世界坐标系中的新坐标信息。 每当刷新主应用循环时,应用都会调用此方法。 因此,我们在这里计算要传递给应用的新看点位置信息,以便在投影到视区之前更新视图矩阵。
void MoveLookController::Update(CoreWindow ^window)
{
// Check for input from the Move control.
if (m_moveInUse)
{
DirectX::XMFLOAT2 pointerDelta(m_movePointerPosition);
pointerDelta.x -= m_moveFirstDown.x;
pointerDelta.y -= m_moveFirstDown.y;
// Figure out the command from the touch-based virtual joystick.
if (pointerDelta.x > 16.0f) // Leave 32 pixel-wide dead spot for being still.
m_moveCommand.x = 1.0f;
else
if (pointerDelta.x < -16.0f)
m_moveCommand.x = -1.0f;
if (pointerDelta.y > 16.0f) // Joystick y is up, so change sign.
m_moveCommand.y = -1.0f;
else
if (pointerDelta.y < -16.0f)
m_moveCommand.y = 1.0f;
}
// Poll our state bits that are 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 45 degree cases are not faster.
DirectX::XMFLOAT3 command = m_moveCommand;
DirectX::XMVECTOR vector;
vector = DirectX::XMLoadFloat3(&command);
if (fabsf(command.x) > 0.1f || fabsf(command.y) > 0.1f || fabsf(command.z) > 0.1f)
{
vector = DirectX::XMVector3Normalize(vector);
DirectX::XMStoreFloat3(&command, vector);
}
// Rotate command to align with our direction (world coordinates).
DirectX::XMFLOAT3 wCommand;
wCommand.x = command.x*cosf(m_yaw) - command.y*sinf(m_yaw);
wCommand.y = command.x*sinf(m_yaw) + command.y*cosf(m_yaw);
wCommand.z = command.z;
// Scale for sensitivity adjustment.
wCommand.x = wCommand.x * MOVEMENT_GAIN;
wCommand.y = wCommand.y * MOVEMENT_GAIN;
wCommand.z = wCommand.z * MOVEMENT_GAIN;
// Our velocity is based on the command.
// Also note that y is the up-down axis.
DirectX::XMFLOAT3 Velocity;
Velocity.x = -wCommand.x;
Velocity.z = wCommand.y;
Velocity.y = wCommand.z;
// Integrate
m_position.x += Velocity.x;
m_position.y += Velocity.y;
m_position.z += Velocity.z;
// Clear movement input accumulator for use during the next frame.
m_moveCommand = DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f);
}
由于当玩家使用基于触摸的移动控制器时,我们不希望抖动,因此我们在指针周围设置一个直径为 32 像素的虚拟死区。 我们还添加了速度,即命令值加上移动增益率。 (你可以根据指针在移动控制器区域中移动的距离调整此行为,以放慢速度或加快移动速度。
当我们计算速度时,我们还将从移动接收的坐标转换为我们发送到计算场景视图矩阵的方法的实际视点的移动。 首先,我们将反转 x 坐标,因为如果我们单击移动或向左或向右拖动外观控制器,则视点在场景的相反方向旋转,因为相机可能会围绕其中央轴旋转。 然后,我们交换 y 轴和 z 轴,因为向上/向下键按下或触摸拖动运动(作为 y 轴行为读取)在移动控制器上应转换为相机操作,将视点移入或移出屏幕(z 轴)。
玩家视点的最后位置是最后位置加计算的速度,呈现器在调用 get_Position 方法(通常在每个帧的设置期间)时将读取此数据。 之后,我们将 move 命令重置为零。
使用相机的新位置更新视图矩阵
我们可以获取相机所关注的场景空间坐标,并且每当告知应用执行此操作时都会更新该坐标(例如,主应用循环中的每 60 秒)。 此伪代码建议你可以实现的调用行为:
myMoveLookController->Update( m_window );
// Update the view matrix based on the camera position.
myFirstPersonCamera->SetViewParameters(
myMoveLookController->get_Position(), // Point we are at
myMoveLookController->get_LookPoint(), // Point to look towards
DirectX::XMFLOAT3( 0, 1, 0 ) // Up-vector
);
祝贺你! 你已在游戏中为触摸屏和键盘/鼠标输入触摸控件实现了基本的移动查找控件!