This page has been translated automatically.
Unigine Basics
1. Introduction
2. Managing Virtual Worlds
3. Preparing 3D Models
4. Materials
5. Cameras and Lighting
7. Making Cutscenes and Recording Videos
8. Preparing Your Project for Release
9. Physics
10. Optimization Basics
11. PROJECT2: First-Person Shooter
12. PROJECT3: Third-Person Cross-Country Arcade Racing Game
13. PROJECT4: VR Application With Simple Interaction

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):

Source code (C++)
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).
Source code (C++)
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:

InputProcessor.h
#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
};
InputProcessor.cpp
#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

Last update: 2024-11-06
Build: ()