了解如何将传统的鼠标和键盘移动和视角控制(也称为鼠标视角控制)添加到 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之间的计算差异。
最后,我们为六个移动自由度定义了六个布尔值,用于指示每个方向移动操作的当前状态(开启或关闭):
- m_forward、 m_back、 m_left、 m_right、 m_up 和 m_down。
我们使用 6 个事件处理程序捕获用于更新控制器状态的输入数据:
- OnPointerPressed。 玩家使用游戏屏幕中的指针按下鼠标左键,或触摸屏幕。
- OnPointerMoved。 玩家在游戏屏幕中使用指针移动鼠标,或在屏幕上拖动触摸指针。
- OnPointerReleased。 玩家在游戏屏幕中使用指针释放了鼠标左键,或停止触摸屏幕。
- OnKeyDown。 玩家按下了一个键。
- OnKeyUp。 玩家释放了一个密钥。
最后,我们使用这些方法和属性来初始化、访问和更新控制器的状态信息。
- 初始化。 我们的应用调用此事件处理程序来初始化控件并将其附加到描述显示窗口的 CoreWindow 对象。
- SetPosition。 我们的应用调用此方法来设置场景空间中控件的 (x、y 和 z) 坐标。
- SetOrientation。 我们的应用调用此方法来设置相机的俯仰和偏航。
- get_Position。 我们的应用访问此属性以获取相机在场景空间中的当前位置。 使用此属性作为将当前相机位置传达给应用的方法。
- get_LookPoint。 我们的应用访问此属性以获取控制器相机当前所对准的点。
- 更新。 读取移动和视角控制器的状态,并更新相机位置。 从应用的主循环中不断调用此方法,以刷新相机控制器数据和场景空间中的相机位置。
现在,你已拥有实现移动和观察控制所需的所有组件。 因此,让我们将这些部分连接在一起。
创建基本输入事件
Windows 运行时事件调度程序提供 5 个事件,我们希望 MoveLookController 类的实例能够处理:
这些事件在 CoreWindow 类型上实现。 我们假设你有一个供操作的 CoreWindow 对象。 如果不知道如何获取,请参阅 如何设置通用 Windows 平台(UWP)C++应用以显示 DirectX 视图。
当这些事件在应用运行时触发时,处理程序将更新在专用字段中定义的控制器的状态信息。
首先,让我们设置鼠标和触摸指针事件处理程序。 在第一个事件处理程序 OnPointerPressed()中,当用户在观察控制区域中单击鼠标或触摸屏幕时,我们从 CoreWindow 中获取指针的 x-y 坐标,该窗口负责管理显示。
按下指针
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;
}
}
}
此事件处理程序检查指针是否不是鼠标(出于此示例的目的,该示例同时支持鼠标和触摸),以及它是否位于移动控制器区域中。 如果这两个条件都为 true,则需要通过测试 m_moveInUse 是否为 false 来检查指针是否刚刚被按下,也就是说,要检查此次单击是否与之前的移动或查看输入无关。 如果是这样,处理程序将记录下按下发生时移动控制器区域内的点,并将 m_moveInUse 设置为 true,这样在再次调用此处理程序时,它将不会覆盖移动控制器输入交互的起始位置。 它还会将移动控制器指针 ID 更新为当前指针的 ID。
如果指针是鼠标,或者触摸指针不在移动控制器区域中,那么它必须位于视图控制器区域中。 它将 m_lookLastPoint 设置为用户按下鼠标按钮或触摸按下时的当前位置,重置增量,并将视图控制器的指针 ID 更新为当前指针 ID。 它还将外观控制器的状态设置为活动状态。
指针移动事件
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 事件处理程序捕获的第一个位置进行比较。
如果是外观控制装置,情况会稍微复杂一些。 我们需要计算一个新的观察点,并将相机对准它。因此,我们计算最后一个观察点与当前屏幕位置之间的差值,然后将其乘以比例因子。我们可以调整比例因子,以便根据屏幕移动的距离,使观察点的移动更小或更大。 使用该值,我们计算俯仰角和偏航。
最后,当玩家停止移动鼠标或触摸屏幕时,我们需要停用移动或视角控制器的功能。 我们使用 OnPointerReleased(在触发 PointerReleased 时调用),将 m_moveInUse 或 m_lookInUse 设置为 FALSE,关闭相机平移,并重置指针 ID 为零。
指针释放
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
);
祝贺! 你已在游戏中为触摸屏及键盘/鼠标输入实现了基本的移动和查看控件!