Show full frameset for easy navigation
Composing vehicles from multiple sprites
OpenTTD since r27668 allows vehicles (trains, road vehicles, ships and aircraft) to be drawn from multiple sprites on top of each other ('stacked sprites'). These sprites can use different recolouring.
To enable this, STACKEDSPRITES needs to be set in the vehicle's property function flags().
Sprite composition is done by function spritestack() which also links real sprites with recolour sprites.
Stacked sprites are resolved multiple times while incrementing the iteration number returned by function spritelayer(). Iteration is currently limited to 4 sprites per articulated part of a vehicle.
This is a simple example how to add back lights to freight cars (or passenger coaches or mail vans), instead of having extra full vehicle graphics with back lights for the last car/coach in a consist.
As usual, the first thing to do is to define a freight car with the stacked sprites feature enabled:
definevehicle(_BOX, {STR_BOX},
newgraphics()
lifecycle(1-1-1900, 1940, 1960)
...
capacity(15) // 15t
flags(STACKEDSPRITES)
callbacks(CB_RCOL)
cargoclasses(+PGOODS, +EXPRESS, -BULK, -LIQUID, -REEF, -TRVL)
)
Next step would be to set up the needed graphics for the freight car and the back lights:
spriteblock(
// closed
set(
template({TEMP8_SBOX},sbox.pcx,x(66,82,114,146,178,194,226,258),y(8))
)
// open
set(
template({TEMP8_SBOX},sbox.pcx,x(66,82,114,146,178,194,226,258),y(27))
)
// back lights (for directions 0,1,7)
set(
sprite(sbox.pcx 66 46 01 1 8 -3 2)
sprite(sbox.pcx 82 46 01 2 5 -10 2)
sprite(sbox.pcx 66 8 01 1 1 0 0)
sprite(sbox.pcx 66 8 01 1 1 0 0)
sprite(sbox.pcx 66 8 01 1 1 0 0)
sprite(sbox.pcx 66 8 01 1 1 0 0)
sprite(sbox.pcx 66 8 01 1 1 0 0)
sprite(sbox.pcx 269 46 01 2 5 7 2)
)
)
Since back lights would only be visible when travelling in upward directions (0,1,7), a single pixel in 'transparent blue' is sufficient for every other direction.
Now, we need sprite sets for both the car body and the extra back lights:
def(0) spriteset(move(0),load(0,1)) // body def(1) spriteset(move(2),load()) // back lights
What we do here is to add back lights only for the moving state, not during loading time, hence function load() does not get any parameter.
Next on our list is to define sprite stacking:
def(2) spritestack(RC_DEFAULT, ref(0), MORE) // body def(3) spritestack(RC_NONE, ref(1)) // back lights
Here, the car body will be recoloured by the current value of recolour callback CB_RCOL and the back lights will use no recolouring at all. Note that parameter MORE will specify that there will be more sprites to be 'stacked'.
At last, we have to determine the order how those sprites should be drawn. This is done by using function spritelayer():
def(4) spritelayer( ref(2) if(0) // first the body ref(3) else // then the back lights )
And since we want back lights to be shown only for the last car in a consist, we need a final decision:
def(5) islast( ref(0) if(NOTLAST) // only body ref(4) else // last car with back lights )
Now everything seems to be completed, time to finish the vehicle:
def(6) callback(
ref(FRC_BOX) if(CB_RCOL) // recolour
ref(5) else // graphics
)
// purchase menu
spriteblock(
set(
sprite(sbox.pcx 10 10 01 12 22 75 -8)
)
)
def(0) spriteset(move(0),load(0))
def(1) callback(
ref(FRC_BOX) if(CB_RCOL) // recolour
grftext(TAL_SBOX) if(CB_TEXT)
ref(0) else
)
makevehicle(_SBOX,
link(ref(1),MENU)
default(ref(6))
)
The basic idea of 'bulk recolouring' is to spare a lot of cargo graphics (either to be drawn and/or to be included in the resulting newGRF, thus reducing its size) by simple recolouring a model bulk graphics. I.e., for each bulk cargo, a cargo 'recolour map' is supplied, e.g. for grain, coal, iron ore, bauxite, etc, and used as required for the currently transported cargo.
In another step, the stacked sprite feature allows to decouple the graphics of the vehicle's body from its cargo graphics, and both sets of graphic sprites could be recoloured independently, if required.
Now, as usual, the first thing to do is to define a freight car with both features (recolouring and stacked sprites) activated:
definevehicle(_SLOW, {STR_LOW},
newgraphics()
lifecycle(1-1-1900, 1940, 1960)
...
capacity(15) // 15t
flags(CARGOMULTIPLIER, STACKEDSPRITES)
callbacks(CB_RCOL, CB_RCAP, CB_TSFX)
cargoclasses(+BULK, +PGOODS, +EXPRESS, -LIQUID, -REEF, -TRVL)
)
Next thing would be to produce the needed recolour maps for bulk cargos. First of all, we define a number of cargo recolourings like this:
define(_BAUXIT,{3F 2C 40 6F 4E 25}) // FIRS
define(_CLAY,{3E 37 23 6E 39 6F}) // FIRS
define(_COAL,{01 02 03 04 05 06})
define(_COPPERORE,{7B 4A 37 5B 1C 4C})
define(_GRAIN,{6D 38 40 41 C3 33})
define(_GRAVEL,{07 38 14 39 09 0A}) // FIRS
define(_IRONORE,{7B 72 4A 73 4B 4C})
...
The hexadecimal numbers in parentheses are colour indices pointing to colour entries in TTD's DOS colour palette, and will be used instead of the 'fake' colours being used as placeholders in the cargo graphic sprites.
Creation of recolour maps is done by setting up a special spriteblock:
spriteblock(ALLOCATE,
set(
...
// +0x85 start of bulk recolouring
colourtable(DOSMAP, 62 .. 67, _GRAIN)
colourtable(DOSMAP, 62 .. 67, _BAUXIT)
colourtable(DOSMAP, 62 .. 67, _CLAY)
colourtable(DOSMAP, 62 .. 67, _COAL)
colourtable(DOSMAP, 62 .. 67, _COPPERORE)
colourtable(DOSMAP, 62 .. 67, _GRAVEL)
colourtable(DOSMAP, 62 .. 67, _IRONORE)
...
)
)
Since we will also need a cargo translation table (CTT) to map cargoes defined in some industry/cargo newGRFs to representations set up in our freight car newGRF, it will be a good idea to make both recolour maps and their cargo entries in the CTT the same in ranks, to make live easier in the following steps:
cargotranslationtable(
{"PASS"}, {"MAIL"}, {"OIL_"}, {"LVST"},
{"GOOD"}, {"WOOD"}, {"STEL"}, {"VALU"},
{"FOOD"}, {"PAPR"}, {"FRUT"}, {"FISH"},
{"WOOL"}, {"GLAS"}, {"DYES"}, {"RFPR"},
{"VEHI"}, {"PETR"}, {"WATR"}, {"BDMT"},
{"FICR"}, {"TOUR"}, {"SALT"}, {"MILK"},
{"MNSP"}, {"FMSP"}, {"ENSP"}, {"BEER"},
{"RCYC"}, {"VPTS"}, {"GRAI"}, {"AORE"},
{"CLAY"}, {"COAL"}, {"CORE"}, {"GRVL"},
{"IORE"}, ....
)
As can be seen, in the CTT the set of to be recoloured bulk cargoes starts with GRAI (which is the label for grain), with the recolour map for grain being number 0x85, directly followed by recolour maps for bauxite, clay, coal, copper ore, grain, gravel, iron ore, ..., etc.
The next big step would be to set up the needed graphics for the freight car and its cargo. Since our freight car will be usable both for bulk and non-bulk, we will set up a sprite block for general bulk and for fruit cargo:
spriteblock(
// [0 .. 4] general BULK
set(
// empty
template({TEMP8_SLOW},db#slow0.pcx, x(66,82,114,146,178,194,226,258),y(8))
)
set(
// 1/4
template({BULK8_SLOW},db#slow0.pcx, x(66,82,114,146,178,194,226,258),y(30))
)
set(
// 1/2
template({BULK8_SLOW},db#slow0.pcx, x(66,82,114,146,178,194,226,258),y(50))
)
set(
// 3/4
template({BULK8_SLOW},db#slow0.pcx, x(66,82,114,146,178,194,226,258),y(70))
)
set(
// full
template({BULK8_SLOW},db#slow0.pcx, x(66,82,114,146,178,194,226,258),y(90))
)
set(
// [5] VOID
template({VOID},db#void0.pcx, x(80,80,80,80,80,80,80,80),y(8))
)
set(
// [6] tarpaulin
template({TEMP8_SLOW},db#slow0.pcx, x(66,82,114,146,178,194,226,258),y(110))
)
// [7 .. 10] FRUT
set(
// 1/4
template({BULK8_SLOW},db#slow1.pcx, x(66,82,114,146,178,194,226,258),y(30))
)
set(
// 1/2
template({BULK8_SLOW},db#slow1.pcx, x(66,82,114,146,178,194,226,258),y(50))
)
set(
// 3/4
template({BULK8_SLOW},db#slow1.pcx, x(66,82,114,146,178,194,226,258),y(70))
)
set(
// full
template({BULK8_SLOW},db#slow1.pcx, x(66,82,114,146,178,194,226,258),y(90))
)
)
We now have defined sprite sets for the car body ('empty'), 4 bulk cargo loading stages, a VOID set, a set with tarpaulin graphics, and 4 cargo loading stages for FRUT, which will not be recoloured:
def(0) spriteset(move(0),load(0)) // empty body, age recoloured (FRC_OPEN) def(1) spriteset(move(5,1,2,3,4,4),load(5,1,2,3,4,4)) // - freight, cargo recoloured def(2) spriteset(move(5,5,5,5,5,6),load(5,5,5,5,5,6)) // - tarpaulin, TTD random
So, def(0) is the empty car body, both for loading and moving. It will be recoloured in a special way, representing different liveries throughout its lifetime.
def(1) is the cargo sprite set, it is showing VOID - 1/4 - 1/2 - 3/4 - full - full, both for loading and moving stages. It will be recoloured according to the current bulk cargo.
def(2) is the graphics set for the tarpaulin, showing VOID - VOID - VOID - VOID - VOID - tarpaulin, both for loading and moving stages. Please note that VOID doesn't show anything, but is primarily used to get the same number of loading/moving stages for stacked graphics. In addition, the tarpaulin sprite is only applied after the full load freight sprite (4) has been shown. The tarpaulin will be recoloured randomly.
Now, let's define the needed functions for sprite composition:
def(3) spritestack(RC_DEFAULT, ref(0), MORE) // body def(4) spritestack(cargomapsprite(0x85 - GRAI), ref(1), MORE) // freight
def(3) is the car's body, the first sprite(set) to draw (ref(0)), RC_DEFAULT means it will be recoloured by the current value of recolour callback CB_RCOL. There will be more sprites to be 'stacked', this is what MORE hints at.
def(4) is the second sprite(set) to draw: the freight sprites (ref(1)), to be recoloured according to the current cargo type transported. This is what cargomapsprite() is used for. There will be still more sprites to be stacked upon this one, so MORE is included again.
We still have to include code for the tarpaulins:
def(5) spritestack(ttdsprite(GREY), ref(2)) def(6) spritestack(ttdsprite(WHITE), ref(2)) def(7) randomrel(LOAD,0,ref(5),ref(6))
def(5) and def(6) define the tarpaulin sprite sets (ref(2)), for grey and white. Recolouring is done by original TTD recolour sprites GREY and WHITE. Since either one of both sprites will be the last one to be drawn, MORE is omitted now.
def(7) makes the random decision based on loading the car anew.
Now, we want show moist-sensitive cargo like grain with tarpaulins and insensitive cargo like coal without:
// with tarpaulin def(10) spritelayer( ref(3) if(0) // body ref(4) if(1) // freight ref(7) else // tarpaulin ) // without tarpaulin def(11) spritelayer( ref(3) if(0) // body ref(4) else // freight )
Next, we have to figure out the code for FRUT, which is also bulk, but is not recoloured, showing "real" graphics:
def(1) spriteset(move(5,7,8,9,10),load(5,7,8,9,10)) // - freight, no recolour def(2) spriteset(move(5,5,5,5,6),load(5,5,5,5,6)) // - tarpaulin, TTD random
We are using the same car body as above,
def(4) spritestack(RC_DEFAULT, ref(0), MORE) // body def(5) spritestack(RC_NONE, ref(1), MORE) // freight
And tarpaulins as above, but with different colours:
// tarpaulins def(7) spritestack(ttdsprite(LGREEN), ref(2)) def(8) spritestack(ttdsprite(LBLUE), ref(2)) def(6) randomrel(LOAD,0,ref(7),ref(8)) def(12) spritelayer( ref(4) if(0) // body ref(5) if(1) // freight ref(6) else // tarpaulin )
Now that the freight-related code is complete, it's time to finish the vehicle:
def(20) veh_cargotype( ref(10) if(GRAI) // bulk with tarpaulin ref(11) if(AORE .. IORE) // bulk without tarpaulin ref(12) if(FRUT) ... ref(13) else // other ) def(21) callback( ref(FRC_OPEN) if(CB_RCOL) // recoloured livery autorefit(1 %) if(CB_RCOST) ref(20) else ) makevehicle(_SLOW, default(ref(21)) )
As already explained in example 2, the first parameter of function spritestack() determines the way a stacked sprite is recoloured. In addition to either no recolouring, recolouring by CB_RCOL, or by use of a certain TTD or custom recolour sprite, it is also possible to carry out random recolouring by specifying a range of recolour sprites as that first parameter.
This is achieved by helper functions randomttdsprite() or randomallocsprite(). Both take a range of either original TTD sprites or of custom recolour sprites, allocated by use of function spriteblock().
Besides some special recolour sprites, TTD supports 16 recolour sprites for company colours (sprite numbers 775 .. 790). Function randomttdsprite() takes a sub-range of them, by adressing TTD sprite 775 by an index of 0, and sprite 790 by 15. I.e., randomttdsprite(0 .. 15) will provide all 16 random recolour sprites (dark blue .. white), but randomttdsprite(14 .. 15) will only provide a random recolouring of either grey or white.
Same method holds true for function randomallocsprite(), except that the usable range of recolour sprites will depend on the number of allocated recolour sprites, see below.
spriteblock(ALLOCATE,
set(
colourtable(DOSMAP, 62 .. 67, _BLUE1)
colourtable(DOSMAP, 62 .. 67, _BLUE2)
colourtable(DOSMAP, 62 .. 67, _RED1)
colourtable(DOSMAP, 62 .. 67, _RED2)
colourtable(DOSMAP, 62 .. 67, _GREEN1)
colourtable(DOSMAP, 62 .. 67, _GREEN2)
colourtable(DOSMAP, 62 .. 67, _GREY1)
colourtable(DOSMAP, 62 .. 67, _GREY2)
)
)
...
def(0) spriteset(move(0),load(0)) // empty body, default recolouring
def(1) spriteset(move(5,1,2,3,4),load(5,1,2,3,4)) // freight, cargo-dependent recolouring
def(2) spriteset(move(5,5,5,5,6),load(5,5,5,5,6)) // tarpaulin, custom random recolouring
def(3) spritestack(RC_DEFAULT, ref(0), MORE) // body
def(4) spritestack(cargomapsprite(0x85 - GRAI), ref(1), MORE) // freight
def(5) spritestack(randomallocsprite(0 .. 7), ref(2)) // tarpaulins
def(6) spritelayer(
ref(3) if(0) // body
ref(4) if(1) // freight
ref(5) else // tarpaulin
)