The m4nfo Tutorial

Show full frameset for easy navigation

Composing vehicles from multiple sprites

Introduction

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.

Example 1: freight cars with back lights

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))
)


Example 2: composing freight cars for bulk by using recolouring and non-bulk graphics

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))
)


Example 3: using random recolouring for stacked sprites

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
)