Processing the Input (keyboard, mouse). Intersections
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.
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):
private void update()
{
// if the right mouse button is pressed
if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
{
// take the current mouse cursor coordinates
ivec2 mouse = Input::getMousePosition();
// write the coordinates to the Console
Log::message("Right mouse button was clicked at (%d, %d)\n", mouse.x, mouse.y);
}
// close the application when the 'q' key is pressed on the keyboard, or ignore it if the Console is open
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:
- World Intersection — intersection with objects and nodes.
- Physics Intersection — intersection with shapes and collision objects.
- Game Intersection — intersection with pathfinding nodes such as obstacles.
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.
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).
int listNodes(Vector<Ptr<Node>>& nodes, const char* intersection_with)
{
Log::message("Total number of nodes intersecting a %s is: %i \n", intersection_with, nodes.size());
for (int i = 0; i < nodes.size(); i++)
{
Log::message("Intersected node: %s \n", nodes.get(i)->getName());
}
// clearing the list of nodes
nodes.clear();
return 1;
}
int AppWorldLogic::update()
{
// getting a player pointer
PlayerPtr player = Game::getPlayer();
// creating a vector to store intersected nodes
Vector<Ptr<Node>> nodes;
//-------------------------- FINDING INTERSECTIONS WITH A BOUNDING BOX -------------------------
// initializing a bounding box with a size of 3 units, located at the World's origin
WorldBoundBox boundBox(Math::Vec3(0.0f), Math::Vec3(3.0f));
// finding nodes intersecting a bounding box and listing them if any
if (World::getIntersection(boundBox, nodes))
listNodes(nodes, "bounding box");
//------------------------- FINDING INTERSECTIONS WITH A BOUNDING SPHERE ------------------------
// initializing a bounding sphere with a radius of 3 units, located at the World's origin
WorldBoundSphere boundSphere(Math::Vec3(0.0f), 3.0f);
// finding nodes intersecting a bounding sphere and listing them if any
if (World::getIntersection(boundSphere, nodes))
listNodes(nodes, "bounding sphere");
//------------------------- FINDING INTERSECTIONS WITH A BOUNDING FRUSTUM -----------------------
// initializing a bounding frustum with a frustum of the player's camera
WorldBoundFrustum boundFrustum(player->getCamera()->getProjection(), player->getCamera()->getModelview());
// finding ObjectMeshStaticNodes intersecting a bounding frustum and listing them if any
if (World::getIntersection(boundFrustum, Node::OBJECT_MESH_STATIC, nodes))
listNodes(nodes, "bounding frustum");
//---------------- FINDING THE FIRST OBJECT INTERSECTED BY A RAY CAST FROM P0 to P1 --------------
// initializing points of the ray from player's position in the direction pointed by the mouse cursor
Math::ivec2 mouse = Input::getMousePosition();
Math::Vec3 p0 = player->getWorldPosition();
Math::Vec3 p1 = p0 + Math::Vec3(player->getDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
//creating a WorldIntersection object to store the information about the intersection
WorldIntersectionPtr intersection = WorldIntersection::create();
// casting a ray from p0 to p1 to find the first intersected object
ObjectPtr obj = World::getIntersection(p0, p1, 1, intersection);
// print the name of the first intersected object and coordinates of intersection point if any
if (obj)
{
Math::Vec3 p = intersection->getPoint();
Log::message("The first object intersected by the ray at point (%f, %f, %f) is: %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.
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:
- Q – Close the application.
- TAB – Send an additional Action (1) signal to the Interactable component on the selected object (for those components that have it implemented).
Let's create a new component, name it InputProcessor, and write the following code in it:
#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:
// declare constructor and destructor for our class and define the name of the property to be associated with the component.
// The InputProcessor.prop file containing all parameters listed below will be generated in your project's data folder after running the app for the first time
COMPONENT_DEFINE(InputProcessor, ComponentBase);
Unigine::WorldIntersectionPtr intersection = nullptr; // intersection point with the last object under the mouse cursor
// registering methods to be called at the corresponding stages of the world logic (methods are declared in the protected-section below)
COMPONENT_INIT(init);
COMPONENT_UPDATE(update);
protected:
// declaration of methods to be called at the corresponding stages of the world logic
void init();
void update();
private:
Unigine::PlayerPtr player = Unigine::Game::getPlayer(); // camera
Unigine::ObjectPtr SelectedObject = nullptr; // last selected object
};
#include "InputProcessor.h"
#include "Interactable.h"
#include <UnigineVisualizer.h>
// registering the InputProcessor component
REGISTER_COMPONENT(InputProcessor);
using namespace Unigine;
using namespace Math;
// component initialization method
void InputProcessor::init()
{
// create an instance of the WorldIntersection class to store information about intersection
intersection = WorldIntersection::create();
}
// component's update method to be called every frame
void InputProcessor::update()
{
// if the console is open, do nothing
if (Unigine::Console::isActive())
return;
// set the beginning of the segment (p0) at the camera position and the end (p1) at the point the mouse cursor is pointing to
ivec2 mouse = Input::getMousePosition();
Vec3 p0 = player->getWorldPosition();
Vec3 p1 = p0 + Vec3(player->getDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
// cast a ray from point p0 to point p1 to find the first intersected object
Unigine::ObjectPtr obj = World::getIntersection(p0, p1, 1, intersection);
// if the object is interactable (has the Interactable component assigned), display information about it on the screen
Interactable* interactable = nullptr;
interactable = ComponentSystem::get()->getComponentInChildren<Interactable>(obj);
if (!interactable)
interactable = ComponentSystem::get()->getComponentInParent<Interactable>(obj);
if (interactable)
interactable->displayInfo();
// check the state of the right mouse button
if (Input::isMouseButtonDown(Input::MOUSE_BUTTON_RIGHT))
{
// if the object is interactable (has the Interactable component assigned)
if (obj && interactable)
{
// send the 'Perform action 0' signal to the object
interactable->action(0);
// register it as the last selected
SelectedObject = obj;
}
}
// check if any object is selected and the 'TAB' key is pressed
if (SelectedObject && Input::isKeyDown(Input::KEY_TAB))
{
// send 'Perform action 1' signal to the object
ComponentSystem::get()->getComponent<Interactable>(SelectedObject)->action(1);
}
// check if the 'Q' key is pressed and the Console is not open, then close the application
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.
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).
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.
Now let's try turning the switches on and off.
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