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

Implementing Controls

Let's create another component named InputController that implements keyboard event handling. We'll make it universal to ensure simple extensibility, for example, it will be easy to add processing of input events from joysticks and other devices.

Let's define a list of events that can occur and their corresponding keys:

  • Hitting the gas (W)
  • Brake (S)
  • Turn (A and D)
  • Hand brake (Spacebar)
  • Position reset (F5)
InputController.h
#pragma once
// include the header file of the Component System
#include <UnigineComponentSystem.h>
#include <UnigineInput.h>
#include <UniginePhysics.h>
#include <UniginePhysics.h>

class InputController : public Unigine::ComponentBase
{
private:
	static InputController* instance;
	static bool enabled;

public:
	// component constructor and the list of methods
	COMPONENT_DEFINE(InputController, ComponentBase);
	// -------------------------------
	COMPONENT_INIT(init, 0);
	COMPONENT_UPDATE(update,0);

	// define a list of action types: pressing gas, brake, turn, hand brake, and position reset
	enum INPUT_ACTION_TYPE
	{
		THROTTLE = 0,
		BRAKE,
		WHEEL_LEFT,
		WHEEL_RIGHT,
		HAND_BRAKE,
		RESET,
	};

	static float getAction(InputController::INPUT_ACTION_TYPE action);
	static InputController* getInstance();
	static bool isEnabled();

	// input unit state interface
	class IInputState
	{
		public:
			virtual ~IInputState() {}
			virtual float getState() { return 0; }
	};

	// interface implementation for pressed key and held down key
	class InputStateKeyboardPressed : public IInputState {
		private:
			Unigine::Input::KEY key = Unigine::Input::KEY::KEY_UNKNOWN;

		public:
			InputStateKeyboardPressed(Unigine::Input::KEY key) { this->key = key; }
			float getState()  {
				return Unigine::Input::isKeyPressed(key) ? 1.0f : 0.0f;
			};
	};

	class InputStateKeyboardDown : public IInputState {

		private:
			Unigine::Input::KEY key = Unigine::Input::KEY::KEY_UNKNOWN;

		public:
			InputStateKeyboardDown(Unigine::Input::KEY key) { this->key = key; }
			float getState() {
				return Unigine::Input::isKeyDown(key) ? 1.0f : 0.0f;
			};
	};

	// abstraction for input action, takes a list of states at initialization
	// and updates its own state, equal to 1.0f, if at least one state from the list is executed
	private: class InputAction
	{
		private: 
			Unigine::Vector <IInputState*> states;

		public:
			float state = 0.0f;

			InputAction(Unigine::Vector <IInputState*> states) { this->states = states; }

			void update()
			{
				float s = -Unigine::Math::Consts::INF; 

				for(int i=0; i < states.size();i++)
				{
					s = Unigine::Math::max(s, states[i]->getState());
				}

				state = s;
			}
	};

	// define a list of input actions for the control keys
	InputAction actions[6] =
	{
		InputAction(Unigine::Vector<IInputState*>({new InputStateKeyboardPressed(Unigine::Input::KEY::KEY_W) })),
		InputAction(Unigine::Vector<IInputState*>({new InputStateKeyboardPressed(Unigine::Input::KEY::KEY_S) })),
		InputAction(Unigine::Vector<IInputState*>({new InputStateKeyboardPressed(Unigine::Input::KEY::KEY_A) })),
		InputAction(Unigine::Vector<IInputState*>({new InputStateKeyboardPressed(Unigine::Input::KEY::KEY_D) })),
		InputAction(Unigine::Vector<IInputState*>({new InputStateKeyboardPressed(Unigine::Input::KEY::KEY_SPACE) })),
		InputAction(Unigine::Vector<IInputState*>({new InputStateKeyboardDown(Unigine::Input::KEY::KEY_F5) })),
	};

protected:
	// main loop overrides
	void init();
	void update();
};
InputController.cpp
#include "InputController.h"
REGISTER_COMPONENT(InputController);

InputController* InputController::instance = nullptr;
bool InputController::enabled;

void InputController::init()
{
	instance = this;
	enabled = true;
}

// update the states of each action every frame
void InputController::update()
{
	for(InputAction action : actions)
	{
		action.update();
	}
}

InputController* InputController::getInstance() 
{
	return instance;
};

float InputController::getAction(InputController::INPUT_ACTION_TYPE action)
{
	if (!enabled)
		return 0.0f;
	if (instance == nullptr)
		return 0.0f;
	instance->actions[(int)action].update();

	return instance->actions[(int)action].state;
}

bool InputController::isEnabled()
{
	return  enabled == true;
}

Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.

Create a new Node Dummy and name it Input. This node will be responsible for processing input events, so assign the InputController component to it.

Now we need to combine the processing of input events and car control, so create the CarPlayer component inherited from Car:

CarPlayer.h
#pragma once
#include "Car.h"
#include "InputController.h"
#include <UnigineComponentSystem.h>
class CarPlayer :
    public Car
{
public:
	// component constructor and the list of methods
	COMPONENT_DEFINE(CarPlayer, Car);
	// -------------------------------
	COMPONENT_UPDATE(update,2);

protected:
	// main loop overrides
	void update();
};
CarPlayer.cpp
#include "CarPlayer.h"
#include "InputController.h"
#include <UnigineComponentSystem.h>
using namespace Unigine;
REGISTER_COMPONENT(CarPlayer);

void CarPlayer::update()
{
	// set wheel rotation
	float wheel = 0.0f;

	wheel -= InputController::getAction(InputController::INPUT_ACTION_TYPE::WHEEL_RIGHT);
	wheel += InputController::getAction(InputController::INPUT_ACTION_TYPE::WHEEL_LEFT);

	setWheelPosition(wheel);

	// brake and throttle
	float brake = InputController::getAction(InputController::INPUT_ACTION_TYPE::BRAKE);
	float throttle = InputController::getAction(InputController::INPUT_ACTION_TYPE::THROTTLE);

	// change driving mode when the speed sign, brake, or throttle condition changes
	float velocity = getSpeed() * (getCurrentMoveDirection() == Car::MOVE_DIRECTION::FORWARD ? 1 : -1);

	if (velocity > 0.0f)
	{
		if (velocity < 1.25f && brake > Math::Consts::EPS && throttle < Math::Consts::EPS)
			setMoveDirection(Car::MOVE_DIRECTION::REVERSE);
	}
	else
	{
		if (velocity > -1.25f && throttle > Math::Consts::EPS && brake < Math::Consts::EPS)
			setMoveDirection(Car::MOVE_DIRECTION::FORWARD);
	}

	// in Reverse mode, throttle and brake are reversed
	if (getCurrentMoveDirection() == Car::MOVE_DIRECTION::REVERSE)
	{
		float t = brake;
		brake = throttle;
		throttle = t;
	}

	// set throttle and brake
	setThrottle(throttle);
	setBrake(brake);
	// set the hand brake using the key and when the input controller is inactive (after the game is over)
	setHandBrake(InputController::isEnabled() ? InputController::getAction(InputController::INPUT_ACTION_TYPE::HAND_BRAKE) : 1.0f);

	Car::update();
}

Save all the files that we modified and then build and run the application by hitting Ctrl + F5 to make the Component System generate a property to be used to assign the components to nodes. Close the application after running it and switch to UnigineEditor.

Assign this component to the pickup_frame node and customize the car parameters as you need, for example use the following values:

Make sure to specify the wheel nodes from the wheels group by dragging each node to the corresponding field. Do the same with the nodes for the brake light and reverse light. The Car component is responsible for turning these lights on and off, so all the lights except the daytime running lights (the light node) should be turned off.

Let's launch the application and enjoy driving.

Last update: 2024-12-13
Build: ()