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.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line 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:

CarPlayer.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;

[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- this line 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 over)
		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.

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