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
${#HL}$ if(Input.IsMouseButtonDown(Input.MOUSE_BUTTON.RIGHT))
{
ivec2 mouse = Input.MousePosition;
// write the current cursor coordinates to the Console
Log.Message("Right mouse button was clicked at {0}\n", mouse);
} ${HL#}$
${#HL}$// close the application when the 'q' key is pressed on the keyboard, or ignore it if the Console is open${HL#}$
if (Input.IsKeyDown(Input.KEY.Q) && !Unigine.Console.Active)
{
${#HL}$Engine.Quit();${HL#}$
}
}
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).
void listNodes(List<Node> nodes, string intersection_with)
{
Log.Message("total number of nodes intersecting a {0} is: {1} \n", intersection_with, nodes.Count);
foreach (Node node in nodes)
{
Log.Message("Intersected node: {0} \n", node.Name);
}
// clearing the list of nodes
nodes.Clear();
}
private void Update()
{
// take a reference to the current camera (Player)
Player player = Game.Player;
// create a list for storing detected nodes
List<Node> nodes = new <Node>();
//-------------------------- FINDING INTERSECTIONS WITH BOUNDING BOX -------------------------
// initializing a bounding box with a size of 3 units located at the World origin
WorldBoundBox boundBox = new WorldBoundBox(new Vec3(0.0f), new Vec3(3.0f));
// searching for nodes intersected the bound and, if found, adding them into the list
if (World.GetIntersection(boundBox, nodes))
listNodes(nodes, "bounding box");
//------------------------- FINDING INTERSECTIONS WITH BOUNDING SPHERE ------------------------
// initializing a bounding sphere with a radius of 3 units at the coordinate origin
WorldBoundSphere boundSphere = new WorldBoundSphere(new Vec3(0.0f), 3.0f);
// searching for nodes intersected the bound and, if found, adding them into the list
if (World.GetIntersection(boundSphere, nodes))
listNodes(nodes, "bounding sphere");
//-------------------------FINDING INTERSECTIONS WITH BOUNDING FRUSTUM -----------------------
// initializing a bounding frustum with a frustum of the player's camera
WorldBoundFrustum boundFrustum = new WorldBoundFrustum(player.Camera.Projection, player.Camera.Modelview);
// finding ObjectMeshStaticNodes intersecting a bounding frustum and listing them if any
if (World.GetIntersection(boundFrustum, Node.TYPE.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 (p0) in the direction pointed by the mouse cursor (p1)
ivec2 mouse = Input.MousePosition;
Vec3 p0 = player.WorldPosition;
Vec3 p1 = p0 + new Vec3(player.GetDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
// creating a WorldIntersection object to store the information about the intersection
WorldIntersection intersection = new WorldIntersection();
// casting a ray from p0 to p1 to find the first intersected object
Unigine.Object obj = World.GetIntersection(p0, p1, 1, intersection);
// print the name of the first intersected object and coordinates of intersection point, if any, to the Console
if (obj)
{
Vec3 p = intersection.Point;
Log.Message("The first object intersected by the ray at point ({0}) is: {1} \n ", p, obj.Name);
}
}
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:
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
#if UNIGINE_DOUBLE
using Vec3 = Unigine.dvec3;
#else
using Vec3 = Unigine.vec3;
#endif
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line is generated automatically for a new component
public class InputProcessor : Component
{
private Player player = Game.Player;
private Unigine.Object SelectedObject = null; // last selected object
public WorldIntersection intersection = null; // intersection point with the last object under the mouse cursor
private void Init()
{
// create an instance of the WorldIntersection class to store information about intersection
intersection = new WorldIntersection();
}
private void Update()
{
// if the console is open, do nothing
if(Unigine.Console.Active)
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.MousePosition;
Vec3 p0 = player.WorldPosition;
Vec3 p1 = p0 + new Vec3(player.GetDirectionFromMainWindow(mouse.x, mouse.y)) * 100;
// cast a ray from point p0 to point p1 to find the first intersected object
Unigine.Object 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 = obj.GetComponent<Interactable>();
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 list 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
SelectedObject.GetComponent<Interactable>().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.Active)
{
Engine.Quit();
}
}
}
Next, let's create an empty NodeDummy, name it input_processor, and assign our component to it (this is a common practice for components with general purpose functionality).
Now let's launch our application by clicking on the Play button and try to turn 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