Controlling vehicles from code
Goal: let a script drive a car, then return control to the player. This chapter assumes you can create a C# MonoBehaviour script and attach it to a GameObject.
For keyboard and gamepad setup, use Controls. For a car that follows a route without writing a driving script, use AI Vehicles.
Before you start
- Use a car that already drives correctly in Your First Drive.
- Keep its Inputs module enabled and Player can drive this car on. This flag also allows scripted driving.
- Test one scripted driver at a time. Disable AI and stop recording, replay and other scripts that control this car.
- Use a clear test area and an already-running engine.
An input is a request such as throttle or steering. An input handle is the object your script receives when it takes control; it lets RCCP reject writes from a script that no longer owns the car.
1. Add a simple scripted driver
Create RCCP_ScriptedDriveExample.cs in your own scripts folder and replace its contents with the example below. Attach it to an empty GameObject, then drag the vehicle root from the Hierarchy into its Car field.
The example holds a small throttle input and drives straight. It returns control when you disable the example component. It is a starting example for a single driver; a mission or network system also needs to manage its own vehicle handoffs.
using UnityEngine;
[DefaultExecutionOrder(-11)]
public class RCCP_ScriptedDriveExample : MonoBehaviour {
[Tooltip("The working player vehicle this example will drive.")]
public RCCP_CarController car;
private RCCP_Input.ControlHandle handle;
private readonly RCCP_Inputs inputs = new RCCP_Inputs();
private void OnEnable() {
if (car == null || car.Inputs == null || !car.canControl) {
Debug.LogWarning("Assign a working car with Inputs and player control enabled.", this);
enabled = false;
return;
}
if (car.externalControl || car.Inputs.IsControlled) {
Debug.LogWarning("Another driver already controls this car.", this);
enabled = false;
return;
}
handle = car.Inputs.AcquireControl(this);
RCCP.SetExternalControl(car, true);
inputs.Clear();
inputs.throttleInput = 0.25f;
}
private void Update() {
if (handle == null || !handle.Set(inputs))
enabled = false;
}
private void OnDisable() {
if (handle != null && handle.IsValid) {
handle.Release();
if (car != null)
RCCP.SetExternalControl(car, false);
}
handle = null;
}
}
2. Try it and return control
- Let Unity compile the script, then check the Console for errors.
- Press Play. Keep your hands off the driving keys while the example runs.
- Select the example's GameObject and untick the component's enabled checkbox.
- Click inside the Game view and try the normal driving controls again.
- Stop Play Mode when finished.
Check: the car drives forward under the script, then accepts player input after you disable the example. If another driver takes control, the example disables itself without releasing that driver's handle.
If the example does not work
| Symptom | Check |
|---|---|
| The component disables itself immediately | Read its Console warning. Assign Car, enable Inputs and player control, and release any other scripted driver. |
| The engine revs but the car does not move | Confirm the car drives normally without the example. Check the engaged gear and vehicle setup. |
| The script stops affecting the car | Another system may have acquired control. Check handle.IsValid and the Console. |
| The player can still operate lights or other vehicle keys in your own script | Acquiring inputs does not set externalControl. Use RCCP.SetExternalControl as the example does. |
| A brake command makes the car reverse | Review input shaping below. Auto-reverse can reinterpret braking when shaping is enabled. |
| Writes succeed during replay but the car does not react | Replay defers live control changes until it ends; see While a replay is running. |
The remaining sections are lookups for extending the example. You do not need to change every flag or use every method.
Taking control of a vehicle
AcquireControl is the supported way to drive a car from a script. One call takes the vehicle, one call gives it back, and the token you get tells you if something else took the car away from you in between.
| Member | Signature | What it does |
|---|---|---|
AcquireControl |
ControlHandle AcquireControl(object owner, bool bypassInputProcessing = true) |
Takes exclusive scripted control and returns a token. Returns null if owner is null. |
ControlHandle.Set |
bool Set(RCCP_Inputs newInputs) |
Writes this tick's inputs. Returns false and writes nothing if the handle is no longer valid. |
ControlHandle.Release |
void Release() |
Returns the vehicle to normal input handling. Safe to call twice; a no-op once the handle is stale. |
ControlHandle.IsValid |
bool |
False once another system acquired the car, a direct override call took over, or the handle was released. |
ControlHandle.Owner |
object |
Whoever holds the car right now, or null. |
IsControlled |
bool |
True while any system holds a token for this vehicle. |
ControlOwner |
object |
The current owner, or null. |
Acquire once, call handle.Set(inputs) every update, and release when done. Reuse an RCCP_Inputs object as the example does instead of allocating a new one every frame.
Pass this as the owner. If that owner is a UnityEngine.Object and it is destroyed while still holding the car, RCCP_Input notices in its next Update, logs a warning naming the vehicle and releases it — otherwise a destroyed mission script would leave the car permanently unresponsive with nothing in the scene left to blame.
A second AcquireControl from a different owner is allowed and logs a warning naming both sides. The first handle stops being valid, so the system that lost the car can detect it rather than silently fighting for the last write of the frame.
What bypassInputProcessing does
It defaults to true, which also raises Skip all input shaping (overrideExternalInputs). This skips the steering curve, steering limiter, counter steering, auto-reverse, pedal swap in reverse and throttle cut during a shift. Deadzones and the final cruise-control/hill-start-assist processing still apply.
Leave it true unless you specifically want the shaping. Passing false leaves the current shaping flag unchanged; it does not force shaping back on. When shaping is active, on a vehicle with an automatic gearbox and auto-reverse on, a full brake input at low speed selects reverse — so a test or cutscene that "brakes" drives backward instead.
Whatever Skip all input shaping was before the first acquire is restored on release, so handing a car between two systems still gives the vehicle back the way it started.
The older override protocol
OverrideInputs and DisableOverrideInputs predate the token and stay fully supported.
| Member | Signature | What it does |
|---|---|---|
OverrideInputs |
void OverrideInputs(RCCP_Inputs overridedInputs) |
Raises Inputs come from a script (overridePlayerInputs) and assigns the inputs object. |
DisableOverrideInputs |
void DisableOverrideInputs() |
Lowers it and returns the car to normal input handling. |
ResetInputs |
void ResetInputs() |
Zeroes the inputs object and all six live axis values. |
Unlike AcquireControl, it never touches Skip all input shaping — set that yourself if you want raw values. A direct OverrideInputs call while someone holds a token takes the car, logs a warning, invalidates that handle and puts Skip all input shaping back to its pre-token value.
The built-in AI uses this protocol: it sets externalControl itself and calls OverrideInputs each tick.
What you write: the inputs class
RCCP_Inputs is a plain serializable class. The default constructor gives all zeros; there is also a seven-argument constructor taking every value below in this order, and a Clear() method that zeroes it in place.
| Field | Range | Meaning |
|---|---|---|
throttleInput |
0 to 1 | 0 = no throttle, 1 = full throttle |
brakeInput |
0 to 1 | 0 = no brake, 1 = full brake |
steerInput |
-1 to 1 | -1 = full left, 0 = center, 1 = full right |
handbrakeInput |
0 to 1 | 0 = released, 1 = fully engaged |
clutchInput |
0 to 1 | 0 = engaged, 1 = fully disengaged |
nosInput |
0 to 1 | 0 = off, 1 = full |
mouseInput |
— | Vector2 delta used for camera orbit, not for driving |
What happens to your values after you write them
Outside replay, RCCP_Input.Update processes device and scripted inputs in this order:
- Release the vehicle if a token owner was destroyed.
- Zero the inputs if Player can drive this car (
canControl) or Driven by a script or AI (externalControl) changed since last frame. - Read the device inputs — skipped while Inputs come from a script is on.
- Apply the per-axis deadzone and copy into the live axis values.
- Apply input shaping — skipped while Skip all input shaping is on.
- Apply cruise control and hill start assist.
Step 4 runs on your values too. Each axis has its own deadzone — throttleDeadzone, steeringDeadzone and the other four all default to 0.05 — and a value above the deadzone is remapped as (value - deadzone) / (1 - deadzone). A scripted throttle of 0.5 therefore reaches the car as 0.474 at the default deadzone. Write 0 and 1 if you need exact values, or zero the deadzones on vehicles that only ever run under script control.
Step 2 is the reason a car goes briefly dead when you flip canControl or externalControl: the inputs object is cleared that frame, so re-apply your values on the next one.
Locking the player out — and the trap in it
Two flags on RCCP_CarController decide whether the player's device reaches the car. Set them through the facade: RCCP.SetControl(vehicle, bool) and RCCP.SetExternalControl(vehicle, bool).
| Flag | Default | Effect when set against the player |
|---|---|---|
Player can drive this car (canControl) |
true |
false stops the car answering the device — and stops it answering your script as well. |
Driven by a script or AI (externalControl) |
false |
true marks the car as script-driven, so it also ignores the light, indicator, gear, engine and assist keys. Your own inputs still get through. |
Turning canControl off is not how you lock the player out of a scripted sequence. Every fixed step the vehicle re-reads its inputs, and while canControl is off it writes zeros over all six of them before the drivetrain ever sees them — the engine reads throttleInput_P, which is one of the values being zeroed. It also applies Apply handbrake (applyHandBrakeOnDisable), which ships on, and optionally Apply brake (applyBrakeOnDisable), which ships off. A cutscene that turns canControl off and then feeds throttle gets a handbraked car that does not move.
Use externalControl instead. It stops the device and the key events without touching your values, which is exactly what the built-in AI does. Leave canControl on for anything you intend to drive yourself.
Neither flag redirects input on its own — you still need a token or an override for your values to be used.
IsControllableByPlayer() is the predicate every device axis and key event is gated on. It requires Player can drive this car on, Driven by a script or AI off, and — in Play Mode, when a player vehicle is registered — that this car is RCCP_SceneManager.activePlayerVehicle. That last condition is what stops every scripted car in the scene toggling its indicators along with the player's.
| Job | Inputs come from a script | Bypass optional input shaping | Player can drive this car | Driven by a script or AI |
|---|---|---|---|---|
| Player driving | off | off | on | off |
| AI or traffic | on | your choice | on | on |
| Cutscene, player locked out | on | on | on | on |
| Remote networked player | on | on | on | on |
| Parked car nobody should move | off | off | off | off |
Reading what the car is doing
Treat the following fields and properties on RCCP_CarController as live readouts. Read them to observe the vehicle; change its configuration or inputs to control it.
| Member | Meaning |
|---|---|
speed |
Road speed in km/h. Signed — negative while reversing. |
absoluteSpeed |
The same value unsigned; a property returning Mathf.Abs(speed). |
wheelRPM2Speed |
Unsigned driven-wheel speed in km/h. Compare with absoluteSpeed; a difference can indicate tire slip, but is not a complete slip diagnosis. |
maximumSpeed |
Top speed the gearing allows in the highest gear at max RPM. |
engineRPM |
Current engine speed in RPM, including zero when stopped. |
currentGear |
Engaged forward gear, counting from 0. |
direction |
1 forward, -1 reverse. |
engineRunning / engineStarting |
Engine state. |
shiftingNow |
True during a gear change. |
reversingNow / NGearNow |
Reverse engaged / neutral engaged. |
steerAngle |
Actual steered-wheel angle in degrees; positive is to the right. |
Input readbacks come in two families, and picking the wrong one is the most common mistake here:
| Suffix | What it is | Use it for |
|---|---|---|
_P |
The player's request after deadzones and driver aids — throttleInput_P, brakeInput_P, steerInput_P, handbrakeInput_P, clutchInput_P, nosInput_P |
Game logic, HUD, scoring |
_V |
What actually reached the components — throttleInput_V, brakeInput_V, steerInput_V, handbrakeInput_V, clutchInput_V, nosInput_V, gearInput_V, fuelInput_V |
Effects and audio that must follow the real vehicle |
The steering curve can reduce both steerInput_P and steerInput_V as speed rises. Neither preserves the original full steering request. If your game needs that request, read it from your driver before RCCP applies input shaping.
Changing gear from code
All five shift methods live on RCCP_Gearbox and all five return immediately and do nothing while shiftingNow is true, so a per-frame call is safe but a queued one is not. A shift takes Shift time (shiftingTime) seconds, default 0.2.
| Method | What it does |
|---|---|
ShiftUp() |
Up one gear. From reverse it goes to gear 0. |
ShiftDown() |
Down one gear. At gear 0 it calls ShiftReverse() instead. |
ShiftToGear(int gear) |
Straight to a forward gear index, or -1 for reverse. |
ShiftReverse() |
Into reverse. Refused while speed is above Max speed to engage reverse (maxSpeedToShiftReverse), default 20 km/h. |
ShiftToN() |
Toggles between neutral and forward. Not a set — calling it twice returns to forward. |
ShiftUp and ShiftDown are additionally refused while forceToNGear is on, and on the Automatic_DNRP transmission unless automaticGearSelector is D (R for ShiftReverse).
Transmission (transmissionType) takes Manual, Automatic or Automatic_DNRP; RCCP.SetAutomaticGear(vehicle, bool) and its TransmissionType overload set it from the facade. The high-level state is currentGearState.gearState, one of Park, InReverseGear, Neutral or InForwardGear.
For full authority — a replay or a custom drivetrain — OverrideGear(int targetGear, float targetGearInput, CurrentGearState.GearState gearState) raises Override gear (overrideGear) and skips all shifting logic until DisableOverride(). While it is on, the automatic shift logic and the rev-match blip are both skipped as well.
While a replay is running
RCCP_ReplayDirector takes the input lock for the length of a playback. Your writes — OverrideInputs, DisableOverrideInputs, ResetInputs, AcquireControl, Release and ControlHandle.Set — are not lost and not applied either: they go to shadow copies and take effect the moment the replay ends, so the car is never driven by two systems at once. ControlHandle.Set still returns true. Check RCCP.IsReplayActive if you need to know.
Common mistakes
| What you would reach for | Why not |
|---|---|
Writing Rigidbody.linearVelocity or angularVelocity to move a car |
Bypasses the whole drivetrain, so grip, assists and audio never see it and the wheels fight you back. Feed inputs instead. |
| Turning Player can drive this car off during a scripted sequence | It zeroes the inputs the drivetrain reads and pulls the handbrake. Use Driven by a script or AI. |
Setting the live throttleInput / steerInput fields on RCCP_Input |
They are outputs, rewritten from the inputs object every frame. Write the inputs object. |
overrideInternalInputs |
Obsolete alias for overridePlayerInputs. |
| Setting Driven by a script or AI and expecting the car to obey you | It only marks the car as non-player. Without a token or an override, nothing is driving it. |
| Calling a shift method in a loop until it takes | It is a no-op while shiftingNow; poll that flag instead of retrying. |
Read next
- The Public API — the facade methods named here, with their full signatures.
- Controls — where device input comes from before any of this applies.
- AI Vehicles — the built-in waypoint AI, which drives cars through the same override path.
- Field Reference — every setting named here, with its default and range.