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:
For a detailed explanation of a fully featured and player-drivable vehicle, see:
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);
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:
In this example, the DataTable is stored in a local Lua table called `GI1E_a`.
---@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. |
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. |
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. |
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). |
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). |
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` |
This section is crucial for:
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", },
Defines exterior cameras if the vehicle supports them. For AI vehicles, this is typically empty.
cameras = { exteriorCameras = {}, },
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). |
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. |
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. |
This section defines headlights and taillights, including:
| 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, }, }, },
The PIS section defines:
Key elements:
(Your provided PIS block remains valid and can be explained further in the next section.)
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. |
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:
Instead of defining the entire vehicle again, we reuse the existing data.
---@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:
This gives us a fully independent vehicle configuration that starts as a 1:1 copy.
GI1E_b.components = GI1E_a.components; -- TODO
Here, the component list is explicitly reused.
This means:
This avoids unnecessary duplication of identical component lists.
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:
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:
Even though most logic is shared, the physical Blueprint can differ.
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`:
This is typical for intermediate cars in a multiple-unit train.
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:
Only the bone names are changed — all other coupling parameters remain the same.
| 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.
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:
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.
---@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 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.
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 }, }, };
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:
Important:
With this section completed, the vehicle and its AI train compositions are fully registered and can be used by SubwaySim 2.
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:
Learn how to build advanced vehicle Blueprints with interactive components, cab logic, and extended systems.
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.