Table of Contents
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:
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.
- vehicle.lua
---@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", },
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:
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.

