====== Creating a Simple Vehicle.lua ======
This page explains how to create a **simple `vehicle.lua` file**, primarily intended for **AI-controlled trains** in SubwaySim 2.
A simple `vehicle.lua` usually defines:
* Vehicle metadata (name, author, IDs)
* Basic physical parameters (mass, length, vmax)
* Blueprint references
* Coupling definitions
* Braking systems
* Basic lights and audio
* Train compositions for AI
For a detailed explanation of a fully featured and player-drivable vehicle, see:
* [[manual:subwaysim:vehicle_development:complex_vehicle_lua|Complex Vehicle.lua]]
----
===== Example Vehicle.lua (AI Vehicle) =====
The following example is based on the **GI1E (Gisela)** AI vehicle used in the SubwaySim base game.
Because the original file is not publicly downloadable, the full structure is shown below and explained step by step in separate sections.
---@type RailVehicle_DataTable
local GI1E_a = {
contentType = "railVehicle",
contentName = "Berlin_GI1E_a",
title = "GI1E",
author = "$GameDeveloper",
emptyMass = 37.0,
vmax = 70,
length = 12.64,
frontToFirstBogie = 2.53,
rearToLastBogie = 2.53,
maxEnginePower = 480,
maxEngineForce = 50,
blueprintFilename = "/SubwaySim_Berlin/Vehicles/GI1E/BP_GI1E_A.BP_GI1E_A",
couplingFront = {
rotationOrigin = "Coupling_F",
couplingOffset = 0.985,
automaticCoupling = true,
skeletalMesh = "Exterior",
couplingBoneName = "Coupling_Rotation1",
},
couplingRear = {
rotationOrigin = "Coupling_B",
couplingOffset = 0.985,
automaticCoupling = true,
skeletalMesh = "Exterior",
couplingBoneName = "Coupling_Rotation2",
},
-- players shouldn't try to attach this vehicle
cameras = {
exteriorCameras = {},
},
brakingSystems = {
pneumatic5bar = {
maxBrakeForce = 60,
},
electric = {
maxBrakeForce = 50,
maxBrakePower = 480,
},
},
components = {
LightManager,
VehicleNumber,
AudioManager,
Berlin_PIS,
},
audio = {
audioBogieAI = {
{
audioComponent = "AiAudio",
},
},
},
lights = {
headLights = {
{
direction = 1,
headlightElements = {
{
mesh = "Exterior",
materialSlot = "GI1E_LightsExterior_mat",
materialParams = {
["Emissive01"] = 100,
},
lightComponents = {
{lightComponent ="Headlight1", intensity = 50, temperature = 4500, attenuationRadius = 500, innerConeAngle = 0, outerConeAngle = 70},
{lightComponent ="Headlight2", intensity = 50, temperature = 4500, attenuationRadius = 500, innerConeAngle = 0, outerConeAngle = 70},
},
},
},
},
},
tailLights = {
{
mesh = "Exterior",
materialSlot = "GI1E_LightsExterior_mat",
direction = -1,
materialParams = {
["Emissive02"] = 100,
},
},
},
},
---@type Berlin_PIS_DataTable
PIS = {
destinationDisplays = {
{
componentName = "ZZA",
direction = 1,
},
},
announcementNextStop = {},
announcementTerminus = {
"/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/General_Termination01.General_Termination01",
},
announcementExitLeft = {},
announcementExitRight = {
"/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/General_ExitRight.General_ExitRight",
},
announcementBell = {
"/SubwaySim_Berlin/Vehicles/Shared/Audio/Gong/Berlin_Standardgong_MasterV1_kurz.Berlin_Standardgong_MasterV1_kurz",
},
announcementTerminusBell = {
"/SubwaySim_Berlin/Vehicles/Shared/Audio/Gong/Berlin_Infogong_MasterV2_kurz.Berlin_Infogong_MasterV2_kurz",
},
announcementDelay = 1.2,
defaultDestinationAnnouncement = "/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/Destination_General.Destination_General",
destinationAnnouncementByLine = {
["U1"] = "/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/Destination_U1.Destination_U1",
["U2"] = "/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/Destination_U2.Destination_U2",
["U3"] = "/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/Destination_U3.Destination_U3",
["U4"] = "/SubwaySim_Berlin/Vehicles/Shared/Audio/Announcements/Destination_U4.Destination_U4",
},
audioComponent = "PIS_Audio",
autoTerminateAnnouncement = false,
},
vehicleNumber = {
labels = {
{
mesh = "Exterior",
materialSlot = "DecalDigits_mat",
},
},
poolName = "Berlin_GI1E",
poolValueMin = 1070,
poolValueMax = 1094,
},
};
---@type RailVehicle_DataTable
local GI1E_b = TableUtil.deepCopy(GI1E_a);
GI1E_b.components = GI1E_a.components; -- TODO
GI1E_b.contentName = "Berlin_GI1E_b";
GI1E_b.blueprintFilename = "/SubwaySim_Berlin/Vehicles/GI1E/BP_GI1E_B.BP_GI1E_B";
GI1E_b.lights.headLights = nil;
GI1E_b.lights.tailLights = nil;
GI1E_b.couplingFront.couplingBoneName = "Coupling_Rotation3";
GI1E_b.couplingRear.couplingBoneName = "Coupling_Rotation4";
---@type TrainComposition_DataTable
local GI1E_1x = {
contentType = "trainComposition",
contentName = "Berlin_GI1e_1x",
title = "GI1e x1",
author = "Simuverse GmbH",
-- not required for AI trains
description = "",
previewFilename = "",
hidden = true,
vehicles = {
{
contentName = "Berlin_GI1E_a",
forward = true,
},
{
contentName = "Berlin_GI1E_b",
forward = true,
},
{
contentName = "Berlin_GI1E_b",
forward = false,
},
{
contentName = "Berlin_GI1E_a",
forward = false,
},
},
};
---@type TrainComposition_DataTable
local GI1E_2x = {
contentType = "trainComposition",
contentName = "Berlin_GI1e_2x",
title = "GI1e x2",
author = "Simuverse GmbH",
-- not required for AI trains
description = "",
previewFilename = "",
hidden = true,
vehicles = {
{
contentName = "Berlin_GI1E_a",
forward = true,
},
{
contentName = "Berlin_GI1E_b",
forward = true,
},
{
contentName = "Berlin_GI1E_b",
forward = false,
},
{
contentName = "Berlin_GI1E_a",
forward = false,
},
{
contentName = "Berlin_GI1E_a",
forward = true,
},
{
contentName = "Berlin_GI1E_b",
forward = true,
},
{
contentName = "Berlin_GI1E_b",
forward = false,
},
{
contentName = "Berlin_GI1E_a",
forward = false,
},
},
};
g_contentManager:addContent(GI1E_a);
g_contentManager:addContent(GI1E_b);
g_contentManager:addContent(GI1E_1x);
g_contentManager:addContent(GI1E_2x);
----
===== RailVehicle_DataTable Explained =====
Now we will go through the `RailVehicle_DataTable` step by step.
The `RailVehicle_DataTable` contains the core configuration of a vehicle.
It defines all information required so the game can correctly:
* load the vehicle Blueprint,
* place the vehicle on the track,
* apply physics and braking,
* configure couplings,
* and register audio, lights, and additional Lua components.
In this example, the DataTable is stored in a local Lua table called `GI1E_a`.
----
==== Creating the Vehicle Data Table ====
---@type RailVehicle_DataTable
local GI1E_a = {
^ Entry ^ Explanation ^
| `local GI1E_a = {` | Creates a **local** Lua table called `GI1E_a`. This table holds all vehicle data and will later be registered via the Content Manager. |
----
==== Basic Metadata ====
These fields define what the vehicle is and how it appears inside the content system.
^ Entry ^ Explanation ^
| `contentType = "railVehicle"` | Defines the content type. For vehicles, this must be `railVehicle`. |
| `contentName = "Berlin_GI1E_a"` | Internal unique identifier used by the Content Manager. Must be unique across all loaded content. |
| `title = "GI1E"` | Display name of the vehicle. |
| `author = "$GameDeveloper"` | Author name shown in content information. You can replace this with your name/project. |
| `description = ""` | Optional description text. Not required for AI-only trains. |
| `previewFilename = ""` | Optional preview image path. Not required for AI-only trains. |
----
==== Physical Parameters ====
These values define physical base properties and basic performance.
^ Entry ^ Explanation ^
| `emptyMass = 37.0` | Empty mass of the vehicle in **tons**. |
| `vmax = 70` | Vehicle maximum speed (km/h) that the vehicle can reach by its own traction. |
----
==== Dimensions and Bogie Distances ====
These values are critical for correct placement on the track and correct bogie positioning.
^ Entry ^ Explanation ^
| `length = 12.64` | Total vehicle length from coupling tip to coupling tip (meters). |
| `frontToFirstBogie = 2.53` | Distance from the **front coupling tip** to the **center of the front bogie** (meters). |
| `rearToLastBogie = 2.53` | Distance from the **rear coupling tip** to the **center of the rear bogie** (meters). |
----
==== Traction / Engine Settings ====
These values define the maximum propulsion capability of the vehicle.
^ Entry ^ Explanation ^
| `maxEnginePower = 480` | Maximum engine power (kW). If you have multiple motors per car, sum them up for the full carbody. |
| `maxEngineForce = 50` | Maximum tractive effort / force (kN). |
----
==== Blueprint Reference ====
This defines which Unreal Blueprint is loaded for this vehicle.
^ Entry ^ Explanation ^
| `blueprintFilename = "/SubwaySim_Berlin/Vehicles/GI1E/BP_GI1E_A.BP_GI1E_A"` | Unreal asset path to the Vehicle Blueprint. Important: Unreal requires the Blueprint name twice at the end, without `.uasset`. Example: `/MyMod/Vehicles/TestTrain/BP_TestTrain.BP_TestTrain` |
----
==== Coupling Configuration ====
This section is crucial for:
* correct vehicle placement on the rail,
* correct coupling rotation,
* automatic coupling behavior in consists.
The front coupling:
couplingFront = {
rotationOrigin = "Coupling_F",
couplingOffset = 0.985,
automaticCoupling = true,
skeletalMesh = "Exterior",
couplingBoneName = "Coupling_Rotation1",
},
^ Entry ^ Explanation ^
| `rotationOrigin` | Name of the **Scene Component** in your Vehicle Blueprint that defines the front coupling reference point (e.g. `Coupling_F`). |
| `couplingOffset` | Offset distance between the coupling reference point and the coupling rotation pivot (meters). |
| `automaticCoupling` | If `true`, coupling happens automatically when approaching another vehicle. |
| `skeletalMesh` | Name of the Skeletal Mesh Component inside the Blueprint that contains the coupling mesh (e.g. `Exterior`). |
| `couplingBoneName` | Name of the coupling rotation bone inside the skeleton (driven by animation). |
The rear coupling works the same way:
couplingRear = {
rotationOrigin = "Coupling_B",
couplingOffset = 0.985,
automaticCoupling = true,
skeletalMesh = "Exterior",
couplingBoneName = "Coupling_Rotation2",
},
{{ :manual:subwaysim:vehicle_lua:image.webp |}}
----
==== Cameras ====
Defines exterior cameras if the vehicle supports them.
For AI vehicles, this is typically empty.
cameras = {
exteriorCameras = {},
},
----
==== Braking Systems ====
Defines available brake systems and their maximum forces.
brakingSystems = {
pneumatic5bar = {
maxBrakeForce = 60,
},
electric = {
maxBrakeForce = 50,
maxBrakePower = 480,
},
},
^ Entry ^ Explanation ^
| `pneumatic5bar.maxBrakeForce` | Maximum brake force of the pneumatic brake (kN). |
| `electric.maxBrakeForce` | Maximum brake force of the electric brake (kN). |
| `electric.maxBrakePower` | Maximum braking power of the electric brake (kW). |
----
==== Components ====
This list defines which Lua components are attached to the vehicle.
components = {
LightManager,
VehicleNumber,
AudioManager,
Berlin_PIS,
},
^ Component ^ Purpose ^
| `LightManager` | Controls vehicle lighting logic. |
| `VehicleNumber` | Handles vehicle number generation and decals. |
| `AudioManager` | Manages audio playback and audio components. |
| `Berlin_PIS` | Controls destination displays and announcements. |
----
==== Audio (AI Bogie Audio) ====
Defines which Blueprint Audio Component is used for AI bogie sound playback.
audio = {
audioBogieAI = {
{
audioComponent = "AiAudio",
},
},
},
^ Entry ^ Explanation ^
| `audioComponent = "AiAudio"` | Name of the Audio Component inside the Vehicle Blueprint that plays the AI MetaSound / audio setup. |
----
==== Lights ====
This section defines headlights and taillights, including:
* which direction they apply to,
* which mesh + material slot is affected,
* which emissive parameters are set,
* and which Light Components are controlled.
^ Concept ^ Explanation ^
| `direction = 1` | Applies when the vehicle faces the forward direction. |
| `direction = -1` | Applies when the vehicle faces the reverse direction. |
| `mesh` | Skeletal Mesh Component name used for material manipulation (e.g. `Exterior`). |
| `materialSlot` | The material slot name that contains emissive parameters. |
| `materialParams` | Material parameter values applied when the light is active (usually emissive strength). |
| `lightComponents` | Actual Unreal Light Components to switch on/off and configure (intensity, cone, radius, etc.). |
Headlights example:
headLights = {
{
direction = 1,
headlightElements = {
{
mesh = "Exterior",
materialSlot = "GI1E_LightsExterior_mat",
materialParams = {
["Emissive01"] = 100,
},
lightComponents = {
{lightComponent ="Headlight1", intensity = 50, temperature = 4500, attenuationRadius = 500, innerConeAngle = 0, outerConeAngle = 70},
{lightComponent ="Headlight2", intensity = 50, temperature = 4500, attenuationRadius = 500, innerConeAngle = 0, outerConeAngle = 70},
},
},
},
},
},
Taillights example:
tailLights = {
{
mesh = "Exterior",
materialSlot = "GI1E_LightsExterior_mat",
direction = -1,
materialParams = {
["Emissive02"] = 100,
},
},
},
----
==== PIS (Destination Displays & Announcements) ====
The PIS section defines:
* which destination display components exist,
* which announcement audio files are used,
* the audio component for playback,
* and additional announcement logic settings.
Key elements:
* `destinationDisplays` defines display components (e.g. `ZZA`)
* audio lists define announcement file assets
* `audioComponent` defines the Blueprint Audio Component used for announcements
(Your provided PIS block remains valid and can be explained further in the next section.)
----
==== Vehicle Numbers ====
Defines how vehicle numbers are generated and displayed.
vehicleNumber = {
labels = {
{
mesh = "Exterior",
materialSlot = "DecalDigits_mat",
},
},
poolName = "Berlin_GI1E",
poolValueMin = 1070,
poolValueMax = 1094,
},
^ Entry ^ Explanation ^
| `labels.mesh` | Mesh that receives the number decal material. |
| `labels.materialSlot` | Material slot where number decals are applied. |
| `poolName` | Name of the number pool. |
| `poolValueMin / poolValueMax` | Range of numbers the vehicle can randomly receive. |
----
===== Creating a Variant Vehicle using deepCopy =====
In this section, we create a **variant** of an existing vehicle by copying an already defined `RailVehicle_DataTable` and adjusting only the values that differ.
This approach is very common and highly recommended, especially for:
* A- and B-cars
* Direction-dependent variants
* Slightly different vehicle bodies sharing the same base configuration
Instead of defining the entire vehicle again, we reuse the existing data.
----
==== Copying the Base Vehicle ====
---@type RailVehicle_DataTable
local GI1E_b = TableUtil.deepCopy(GI1E_a);
This line creates a **deep copy** of the previously defined vehicle `GI1E_a`.
Important points:
* All values from `GI1E_a` are copied into `GI1E_b`
* Nested tables (lights, brakes, couplings, etc.) are copied as well
* Changes made to `GI1E_b` do **not** affect `GI1E_a`
This gives us a fully independent vehicle configuration that starts as a 1:1 copy.
----
==== Reusing Component Definitions ====
GI1E_b.components = GI1E_a.components; -- TODO
Here, the component list is explicitly reused.
This means:
* Both vehicles use the same Lua components (LightManager, AudioManager, PIS, etc.)
* This is safe as long as the component behavior is identical
* The comment `-- TODO` indicates that this could be changed later if needed
This avoids unnecessary duplication of identical component lists.
----
==== Changing the Content Name ====
GI1E_b.contentName = "Berlin_GI1E_b";
Each vehicle must have a **unique `contentName`**.
Even though `GI1E_b` is based on `GI1E_a`, it must be registered as a separate vehicle in the Content Manager.
This name is later used:
* in train compositions
* when spawning vehicles
* internally by the game
----
==== Assigning a Different Blueprint ====
GI1E_b.blueprintFilename = "/SubwaySim_Berlin/Vehicles/GI1E/BP_GI1E_B.BP_GI1E_B";
The B-car uses a **different Vehicle Blueprint**.
Typical reasons for this:
* Different interior layout
* No driver cab
* Different light setup
* Different coupling setup
Even though most logic is shared, the physical Blueprint can differ.
----
==== Removing Head- and Taillights ====
GI1E_b.lights.headLights = nil;
GI1E_b.lights.tailLights = nil;
In this case, the B-car does **not** have its own headlights or taillights.
By setting these entries to `nil`:
* The light definitions inherited from `GI1E_a` are removed
* The vehicle will not attempt to control head- or taillights
* This prevents duplicated or incorrect lighting inside a train consist
This is typical for intermediate cars in a multiple-unit train.
----
==== Adjusting Coupling Bone Names ====
GI1E_b.couplingFront.couplingBoneName = "Coupling_Rotation3";
GI1E_b.couplingRear.couplingBoneName = "Coupling_Rotation4";
The B-car uses **different coupling bones** than the A-car.
Reasons for this include:
* Different skeleton layout
* Additional coupling bones for internal train connections
* Separate animation channels for different couplers
Only the bone names are changed — all other coupling parameters remain the same.
----
==== Summary: Differences Between GI1E_a and GI1E_b ====
^ Aspect ^ GI1E_a (A-car) ^ GI1E_b (B-car) ^
| Base data | Fully defined | Copied from GI1E_a |
| Blueprint | BP_GI1E_A | BP_GI1E_B |
| Lights | Head- and tail lights present | No head- or tail lights |
| Coupling bones | Rotation1 / Rotation2 | Rotation3 / Rotation4 |
| Components | Own list | Reused from GI1E_a |
| contentName | Berlin_GI1E_a | Berlin_GI1E_b |
Using `TableUtil.deepCopy` keeps your Lua files clean, maintainable, and easy to extend when creating multiple vehicle variants.
----
===== Train Compositions and Content Registration =====
At the end of the file comes one of the most important parts:
**Train compositions** and the registration of all DataTables in the **Content Manager**.
Without these steps, the game would know that the vehicle exists as a data table — but it would not know:
* how to spawn it as a full train consist,
* and it would not load the content at all.
----
==== What is a TrainComposition_DataTable? ====
A `TrainComposition_DataTable` defines a **complete train consist** (multiple vehicles in a fixed order).
AI trains in SubwaySim 2 are usually spawned using **train compositions**, not single vehicles.
That is why this section is essential for AI vehicles.
----
==== Example: GI1E_1x (one train unit) ====
---@type TrainComposition_DataTable
local GI1E_1x = {
contentType = "trainComposition",
contentName = "Berlin_GI1e_1x",
title = "GI1e x1",
author = "Simuverse GmbH",
-- not required for AI trains
description = "",
previewFilename = "",
hidden = true,
vehicles = {
{ contentName = "Berlin_GI1E_a", forward = true },
{ contentName = "Berlin_GI1E_b", forward = true },
{ contentName = "Berlin_GI1E_b", forward = false },
{ contentName = "Berlin_GI1E_a", forward = false },
},
};
^ Field ^ Meaning ^
| `contentType = "trainComposition"` | Defines that this table is a train composition. |
| `contentName` | Internal unique identifier for this consist. Used by the Content Manager and spawners. |
| `title` | Display name (used in menus or debug lists). |
| `author` | Author name. |
| `hidden = true` | Hides this composition from the vehicle selection menu. Very common for AI-only trains. |
| `vehicles` | The ordered list of vehicles that form the train consist. |
----
==== The vehicles list ====
The most important part is the `vehicles = { ... }` block.
Each entry uses:
^ Field ^ Meaning ^
| `contentName` | Refers to a vehicle previously defined and registered (e.g. `Berlin_GI1E_a`). |
| `forward` | Defines the orientation of that vehicle inside the consist. `true` means forward-facing, `false` means reversed. |
This is where you can clearly see why `contentName` is so important:
It acts as the unique key that links your **vehicle DataTables** into a full train consist.
----
==== Example: GI1E_2x (two units coupled) ====
The 2x consist simply repeats the same unit again, resulting in a longer AI train.
---@type TrainComposition_DataTable
local GI1E_2x = {
contentType = "trainComposition",
contentName = "Berlin_GI1e_2x",
title = "GI1e x2",
author = "Simuverse GmbH",
-- not required for AI trains
description = "",
previewFilename = "",
hidden = true,
vehicles = {
{ contentName = "Berlin_GI1E_a", forward = true },
{ contentName = "Berlin_GI1E_b", forward = true },
{ contentName = "Berlin_GI1E_b", forward = false },
{ contentName = "Berlin_GI1E_a", forward = false },
{ contentName = "Berlin_GI1E_a", forward = true },
{ contentName = "Berlin_GI1E_b", forward = true },
{ contentName = "Berlin_GI1E_b", forward = false },
{ contentName = "Berlin_GI1E_a", forward = false },
},
};
----
==== Registering Content with the Content Manager ====
At the very end, we must register all content tables so SubwaySim 2 actually loads them.
g_contentManager:addContent(GI1E_a);
g_contentManager:addContent(GI1E_b);
g_contentManager:addContent(GI1E_1x);
g_contentManager:addContent(GI1E_2x);
This does two things:
* It tells the game: **"These DataTables exist and should be loaded."**
* It makes the `contentName` entries usable by the spawning and AI systems.
Important:
* We pass the **local Lua variables** here (e.g. `GI1E_a`), not the string names.
* If you forget to register a table, it will not exist in the game content system.
----
==== Summary ====
* `RailVehicle_DataTable` defines **single vehicles**
* `TrainComposition_DataTable` defines **AI train consists**
* `g_contentManager:addContent(...)` is required so SubwaySim 2 loads the vehicles and compositions at all
With this section completed, the vehicle and its AI train compositions are fully registered and can be used by SubwaySim 2.
----
===== What's Next? =====
This page covered the setup of a **simple vehicle.lua**, mainly intended for AI-operated trains.
If you want to create more advanced vehicles — such as player-driven trains, vehicles with interactive cabs, advanced systems, or extended logic — you will find further detailed information in the following chapters:
* **[[manual:subwaysim:vehicle_development:complex_vehicle_blueprint|Complex Vehicle Blueprint]]**
Learn how to build advanced vehicle Blueprints with interactive components, cab logic, and extended systems.
* **[[manual:subwaysim:vehicle_development:complex_vehicle_lua|Complex Vehicle.lua]]**
Explore advanced Lua setups including player controls, extended systems, and complex vehicle behavior.
These chapters expand on the foundations explained here and guide you step by step towards fully-featured, player-ready vehicles.
{{page>manual:footer}}