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)
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- identifier is generated automatically for a new component
public class InputController : Component
{
// this component is represented by a singleton for easier access
private static InputController instance = null;
// define a list of action types: pressing gas, brake, turn, hand brake and position reset
public enum InputActionType
{
Throttle = 0,
Brake,
WheelLeft,
WheelRight,
HandBrake,
Reset,
}
// input unit state interface
private interface IInputState
{
float State { get; }
}
// interface implementation for pressed key and held down key
private class InputStateKeyboardPressed : IInputState {
private Input.KEY key = Input.KEY.UNKNOWN;
public InputStateKeyboardPressed(Input.KEY key) { this.key = key; }
float IInputState.State { get { return Input.IsKeyPressed(key) ? 1.0f : 0.0f; } }
}
private class InputStateKeyboardDown : IInputState {
private Input.KEY key = Input.KEY.UNKNOWN;
public InputStateKeyboardDown(Input.KEY key) { this.key = key; }
float IInputState.State { get { return 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 IInputState[] states;
private float state = 0.0f;
public InputAction(IInputState[] states) { this.states = states; }
public void Update()
{
float s = float.NegativeInfinity;
foreach(IInputState state in states)
{
s = MathLib.Max(s,state.State);
}
state = s;
}
public float State { get { return state; } }
}
// define a list of input actions for the control keys
private InputAction[] actions =
{
new InputAction(new IInputState[] { new InputStateKeyboardPressed(Input.KEY.W) }),
new InputAction(new IInputState[] { new InputStateKeyboardPressed(Input.KEY.S) }),
new InputAction(new IInputState[] { new InputStateKeyboardPressed(Input.KEY.A) }),
new InputAction(new IInputState[] { new InputStateKeyboardPressed(Input.KEY.D) }),
new InputAction(new IInputState[] { new InputStateKeyboardPressed(Input.KEY.SPACE) }),
new InputAction(new IInputState[] { new InputStateKeyboardDown(Input.KEY.F5) }),
};
// the input controller will be initialized first — due to the explicitly specified order
[Method(Order=0)]
private void Init()
{
instance = this;
IsEnabled = true;
}
// update the states of each action every frame
private void Update()
{
foreach(InputAction action in actions)
{
action.Update();
}
}
public static float GetAction(InputActionType action)
{
if (!IsEnabled)
return 0.0f;
if (instance == null)
return 0.0f;
return instance.actions[(int)action].State;
}
public static bool IsEnabled { get; set; }
}
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:
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- identifier is generated automatically for a new component
public class CarPlayer : Car
{
protected override void Update()
{
// set wheel rotation
float wheel = 0.0f;
wheel -= InputController.GetAction(InputController.InputActionType.WheelRight);
wheel += InputController.GetAction(InputController.InputActionType.WheelLeft);
SetWheelPosition(wheel);
// brake and throttle
float brake = InputController.GetAction(InputController.InputActionType.Brake);
float throttle = InputController.GetAction(InputController.InputActionType.Throttle);
// change driving mode when the speed sign, brake, or throttle condition changes
float velocity = Speed * (CurrentMoveDirection == MoveDirection.Forward ? 1 : -1);
if (velocity > 0.0f)
{
if (velocity < 1.25f && brake > MathLib.EPSILON && throttle < MathLib.EPSILON)
SetMoveDirection(MoveDirection.Reverse);
}
else
{
if (velocity > -1.25f && throttle > MathLib.EPSILON && brake < MathLib.EPSILON)
SetMoveDirection(MoveDirection.Forward);
}
// in Reverse mode, throttle and brake are reversed
if (CurrentMoveDirection == MoveDirection.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 finished)
SetHandBrake(InputController.IsEnabled ? InputController.GetAction(InputController.InputActionType.HandBrake) : 1.0f);
base.Update();
}
}
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.