处理输入(键盘、鼠标)。碰撞
Now we have the first interactive objects, but no interaction yet.现在我们已经有了第一个可交互对象,但尚未实现交互功能。
The good old way of interacting with the user is processing input from the user received from different devices (mouse, keyboard, joystick, etc.). The main class in charge of all this in UNIGINE is Input class.传统的用户交互方式是通过处理来自不同设备(鼠标、键盘、游戏手柄等)的输入。在UNIGINE中负责这一切的主要类是Input类。
The following code illustrates how to use the Input class to get the cursor coordinates when the right mouse button is pressed, and to close the application when the "q" key on the keyboard is pressed (ignoring this key if the Console is open):以下代码展示了如何使用Input类在鼠标右键按下时获取光标坐标,以及在键盘"q"键按下时关闭应用程序(如果控制台打开则忽略该键):
private void update()
{
// 检测鼠标右键是否按下
if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
{
// 获取当前鼠标光标坐标
ivec2 mouse = Input::getMousePosition();
// 将坐标输出到控制台
Log::message("鼠标右键点击坐标:(%d, %d)\n", mouse.x, mouse.y);
}
// 当键盘Q键按下时关闭应用程序(控制台未激活状态下)
if (Input::isKeyDown(Input::KEY_Q) && !Unigine::Console::isActive())
{
Engine::get()->quit();
}
}
Intersections are widely used in 3D applications for a wide range of tasks, such as selecting objects with the mouse, simplified modeling of car wheels, identifying objects and players caught in the blast zone and much more. There are three main types of intersections in UNIGINE:交集(Intersections,碰撞检测)在 3D 应用中被广泛应用于各种任务,例如用鼠标选择对象、简化汽车轮子的建模、识别处于爆炸区域的物体和玩家等等。在 UNIGINE 中主要有三种类型的交集检测:
- World Intersection — intersection with objects and nodes.World Intersection:与对象和节点的相交
- Physics Intersection — intersection with shapes and collision objects.Physics Intersection:与形状和碰撞对象的相交
- Game Intersection — intersection with pathfinding nodes such as obstacles.Game Intersection:与寻路节点(如障碍物)的相交
However, there are conditions that should be fulfilled to ensure surface intersection detection:但要确保能够检测到表面相交,需要满足以下条件:
- The surface must be enabled.表面必须启用
- The surface must have a material assigned.表面必须分配了材质
- The Intersection flag must be enabled for each surface. This can be done via the API using the Object::setIntersection() method.必须为每个表面启用Intersection(相交)标志。这可以通过API使用Object::setIntersection()方法来完成。
The code below shows several usage examples for intersections:下面的代码展示了几个相交检测的使用示例:
- Finding all nodes intersected by a bounding box.查找与边界框相交的所有节点
- Finding all nodes intersected by a bounding sphere.查找与边界球体相交的所有节点
- Finding all nodes intersected by a bounding frustum.查找与边界视锥体相交的所有节点
- Finding the first object intersected with a ray (raycast).查找与射线相交的第一个对象(raycasting,射线投射)
int listNodes(Vector<Ptr<Node>>& nodes, const char* intersection_with)
{
Log::message("与 %s 相交的节点总数: %i \n", intersection_with, nodes.size());
for (int i = 0; i < nodes.size(); i++)
{
Log::message("相交节点: %s \n", nodes.get(i)->getName());
}
// 清空节点列表
nodes.clear();
return 1;
}
int AppWorldLogic::update()
{
// 获取玩家(Player)指针
PlayerPtr player = Game::getPlayer();
// 创建存储相交节点的向量
Vector<Ptr<Node>> nodes;
//-------------------------- 边界框相交检测 -------------------------
// 初始化位于世界原点、大小为3个单位的边界框
WorldBoundBox boundBox(Math::Vec3(0.0f), Math::Vec3(3.0f));
// 检测与边界框相交的节点并列出
if (World::getIntersection(boundBox, nodes))
listNodes(nodes, "bounding box");
//------------------------- 边界球体相交检测 ------------------------
// 初始化位于世界原点、半径为3个单位的边界球体
WorldBoundSphere boundSphere(Math::Vec3(0.0f), 3.0f);
// 检测与边界框相交的节点并列出
if (World::getIntersection(boundSphere, nodes))
listNodes(nodes, "bounding sphere");
//------------------------- 视锥体相交检测 -----------------------
// 使用玩家摄像机投影矩阵初始化视锥体
WorldBoundFrustum boundFrustum(player->getCamera()->getProjection(), player->getCamera()->getModelview());
// 检测ObjectMeshStatic(静态网格对象)与视锥体的相交
if (World::getIntersection(boundFrustum, Node::OBJECT_MESH_STATIC, nodes))
listNodes(nodes, "bounding frustum");
//---------------- 从 P0 到 P1 发射射线查找第一个相交的对象 --------------
// 初始化射线的起点(p0)为玩家位置,方向为鼠标光标指向的方向(p1)
Math::ivec2 mouse = Input::getMousePosition();
Math::Vec3 p0 = player->getWorldPosition();
Math::Vec3 p1 = p0 + Math::Vec3(player->getDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
// 创建一个 WorldIntersection 对象,用于存储相交信息
WorldIntersectionPtr intersection = WorldIntersection::create();
// 从 p0 到 p1 发射射线,查找第一个相交的对象
ObjectPtr obj = World::getIntersection(p0, p1, 1, intersection);
// 如果检测到对象,输出对象名称和相交点坐标
if (obj)
{
Math::Vec3 p = intersection->getPoint();
Log::message("射线在点 (%f, %f, %f) 处首次相交的对象是: %s \n ", p.x, p.y, p.z, obj->getName());
}
return 1;
}
Interacting with objects in the scene using the mouse usually involves detecting the object under the cursor. The last example (raycast) is used for this purpose. We cast a ray from the position of the observer (Player) in the direction of the mouse cursor coordinates and look for the first object intersected by our ray. 在场景中使用鼠标与对象交互通常需要检测光标下的对象。最后一个示例(射线投射)就是用于此目的。我们从观察者(Player)位置向鼠标光标坐标方向发射一条射线,并寻找与射线相交的第一个对象。
Practice实践#
To test out interactive objects in our application, we desperately need the component implementing this functionality, processing the mouse click, and sending the Action signal (user action) on the selected object to the Interactable component. In addition, the component will handle the keystrokes:为了测试我们应用中的交互对象,我们迫切需要一个组件来实现此功能:处理鼠标点击,并将选中的对象上的Action(操作)信号(用户操作)发送给 Interactable 组件。此外,该组件还将处理按键输入:
- Q – Close the application.Q:关闭应用程序
- TAB – Send an additional Action (1) signal to the Interactable component on the selected object (for those components that have it implemented).TAB:向选中对象的Interactable组件发送额外Action (1)信号(针对已实现该功能的组件)
Let's create a new component, name it InputProcessor, and write the following code in it:让我们创建一个名为InputProcessor:
#pragma once
#include <UnigineComponentSystem.h>
#include <UnigineGame.h>
#include <UniginePlayers.h>
#include <UnigineWorld.h>
#include <UnigineConsole.h>
#include <UnigineEngine.h>
class InputProcessor :
public Unigine::ComponentBase
{
public:
// 声明构造函数和析构函数,并定义与该组件关联的属性文件名。
// 在首次运行应用程序后,InputProcessor.prop 文件(包含下方列出的所有参数)将被生成到项目的 data 文件夹中
COMPONENT_DEFINE(InputProcessor, ComponentBase);
Unigine::WorldIntersectionPtr intersection = nullptr; // 鼠标光标下与最后一个对象的交点
// 注册在逻辑流程中对应阶段被调用的方法(方法在 protected 部分声明)
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
protected:
// 声明将在世界逻辑的相应阶段调用的方法
void init();
void update();
private:
Unigine::PlayerPtr player = Unigine::Game::getPlayer(); // 摄像机
Unigine::ObjectPtr SelectedObject = nullptr; // 上一次被选中的对象
};
#include "InputProcessor.h"
#include "Interactable.h"
#include <UnigineVisualizer.h>
// 注册 InputProcessor 组件
REGISTER_COMPONENT(InputProcessor);
using namespace Unigine;
using namespace Math;
// 组件初始化方法
void InputProcessor::init()
{
// 创建 WorldIntersection 实例,用于存储交点信息
intersection = WorldIntersection::create();
}
// 每帧调用的组件更新方法
void InputProcessor::update()
{
// 如果控制台开启,什么也不做
if (Unigine::Console::isActive())
return;
// 设置线段起点 p0 为摄像机位置,终点 p1 为鼠标指向的方向
ivec2 mouse = Input::getMousePosition();
Vec3 p0 = player->getWorldPosition();
Vec3 p1 = p0 + Vec3(player->getDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
// 从 p0 到 p1 发射射线,查找第一个相交对象
Unigine::ObjectPtr obj = World::getIntersection(p0, p1, 1, intersection);
// 如果对象可交互(分配了 Interactable 组件),则在屏幕上显示其信息
Interactable* interactable = nullptr;
interactable = ComponentSystem::get()->getComponentInChildren<Interactable>(obj);
if (!interactable)
interactable = ComponentSystem::get()->getComponentInParent<Interactable>(obj);
if (interactable)
interactable->displayInfo();
// 检查鼠标右键是否按下
if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
{
// 如果对象可交互(分配了 Interactable 组件)
if (obj && interactable)
{
// 向对象发送“执行操作 0”的信号
interactable->action(0);
// 将其注册为上一次被选中的对象
SelectedObject = obj;
}
}
// 如果有对象被选中,并按下了 TAB 键
if (SelectedObject && Input::isKeyDown(Input::KEY_TAB))
{
// 向对象发送“执行操作 1”的信号
ComponentSystem::get()->getComponent<Interactable>(SelectedObject)->action(1);
}
// 如果按下 Q 键并且控制台未开启,则关闭应用
if (Input::isKeyDown(Input::KEY_Q) && !Unigine::Console::isActive())
{
Engine::get()->quit();
}
}
Let's save our files and then build and run our application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign our component to nodes. Close the application after running it and switch to UnigineEditor.请保存文件后按下 Ctrl + F5 编译并运行应用程序,使组件系统生成用于将组件分配给节点的属性文件。运行完成后关闭应用,切换至UnigineEditor。
Next, let's create an empty NodeDummy, name it input_processor, and assign our new generated InputProcessor property to it (this is a common practice for components with general purpose functionality).接下来,创建一个空的NodeDummy节点,命名为input_processor,然后将新生成的InputProcessor属性分配给该节点(这是通用功能组件的标准做法)。
Save the world by hitting Ctrl + S. Switch to SDK Browser and change the default world for our application in Customize Run Options, by clicking an ellipsis under the Run button on the project's card (type -console_command "world_load archviz" in the Arguments field then check Remember and click Run to launch our application by clicking the Run button.按Ctrl + S保存场景。切换到SDK Browser,通过点击项目卡片上Run(运行)按钮下方的省略号图标,在Customize Run Options(自定义运行选项)中更改应用程序的默认世界。在Arguments(参数)字段中输入 -console_command "world_load archviz",勾选Remember(记住)选项,然后点击Run(运行)按钮启动应用程序。
Now let's try turning the switches on and off (use the right mouse button, in case you've accidentally left-clicked - press ESC on the keyboard to release the grabbed mouse cursor).现在让我们尝试开关操作(使用鼠标右键点击,如果你意外左键点击了,按键盘上的ESC键释放捕获的鼠标光标)。
After launching the application, don't forget to enable Visualizer to see the prompts on the screen — just open the console and type: show_visualizer 2启动应用程序后,别忘了启用Visualizer以查看屏幕上的提示:只需打开控制台并输入 show_visualizer 2。
本页面上的信息适用于 UNIGINE 2.20 SDK.