NMLTutorial/Object graphics
The example used here is from the Dutch Road Furniture. The original graphics for this are by FooBar. The code is by FooBar, based on code for the object example from the NML source by planetmaker and Hirundu. Code and graphics are both licensed according to the GPL v2 or later. The code has been modified for the purpose of this tutorial.
This continues the first part of the object example. In this last part we'll add the graphics to the object.
The graphics
Also this time we'll give you the graphics.
Templates
Because we'll only be using one spriteset block for these graphics, templating them isn't really necessary. Because the graphics come from a NewGRF that has multiple fingerpost graphics, the template exist so we'll use it here too. After all, in case of multiple similar graphics a template is useful and if you want you can use it for yourself:
//templates template template_fingerpost(x,y,filename) { [x, y, 20, 32, -10, -28, filename] [x+30, y, 20, 32, -10, -28, filename] [x+60, y, 20, 32, -10, -28, filename] [x+90, y, 20, 32, -10, -28, filename] }
This template uses three arguments, the usual x and y arguments for the position within the graphics file, but this time we've also added an argument for the filename. This way you can have graphics from multiple graphics files in a single spriteset block. Not really necessary for this example, but it is useful in the source (see link at top of page) where this example was taken from.
Spritesets
As indicated, we'll have one spriteset block:
spriteset (spriteset_fingerpost_3) { template_fingerpost(0,0,"gfx/dutch_fingerpost.png") }
The graphics path and filename is now omitted from the arguments of the spriteset (only identifier is left), but added to the arguments of the template.
Spritelayouts
There are four different sprites for the fingerpost, one for each rotation. We'll use one sprite for each view of the object, so we need four spritelayout blocks. Each spritelayout will get a ground block for the ground sprite and a building block with a bounding box for the fingerpost sprite. Because that's all the sprites we need, there will be no childsprites necessary.
Let's start with the first spritelayout block for the first orientation of the fingerpost:
//south east spritelayout spritelayout_fingerpost_3_SE { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(0); xextent: 4; yextent: 4; zextent: 24; xoffset: 6; //from NE edge yoffset: 12; //from NW edge zoffset: 0; } }
For the ground sprite we don't need any fancy stuff such as recolouring, hiding or handling transparency differently, so we'll only have a sprite
entry here. For this we used the GROUNDSPRITE_NORMAL
pre-defined constant, which will give us a flat tile for the default terrain. In temperate climate it is the default grass tile, in arctic the arctic grass tile, in tropic the tropic grass tile and in toyland the default toyland tile. Snow and desert need special handling, which we'll look into later when also adding the sloped tile support.
For the building sprite we reference the first sprite of the spriteset block using the spriteset's identifier with an added argument: spriteset_fingerpost_3(0)
. The 0
as argument refers to the first sprite of the spriteset (counting starts at 0).
The bounding box (extent entries) is set to a quarter of the tile length in both x and y directions (tile is 16 long) and has a height of 24 pixels. The bounding box is repositions (offset entries) from it's default position in the top corner of the tile. These values put this particular bounding box at the middle of the southeast tile edge. Because half the width of the bounding box is 2, an offset of 6 will put it in the middle and an offset of 12 at the opposite edge.
From there you can fill in the other spritelayout blocks yourself. These are all the same, except for the offsets because we want the object in a different position on it's tile for each orientation:
//south west spritelayout spritelayout_fingerpost_3_SW { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(1); xextent: 4; yextent: 4; zextent: 24; xoffset: 12; //from NE edge yoffset: 6; //from NW edge zoffset: 0; } } //north west spritelayout spritelayout_fingerpost_3_NW { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(2); xextent: 4; yextent: 4; zextent: 24; xoffset: 6; //from NE edge yoffset: 0; //from NW edge zoffset: 0; } } //north east spritelayout spritelayout_fingerpost_3_NE { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(3); xextent: 4; yextent: 4; zextent: 24; xoffset: 0; //from NE edge yoffset: 6; //from NW edge zoffset: 0; } }
Attaching the graphics with a switch block
Now each view has a different spritelayout block. Because there is four and not one spritelayout block, we cannot reference it directly form the graphics block (of the item block). We have to use an intermediate switch block for that. Reference that from the graphics block for the default "callback":
graphics { default: switch_fingerpost_3_view; }
The switch block will have to make a decision based on the four views of the object. The view
object variable is used for that. This starts counting at 0 up to 3 for all four views.
That makes the switch block quite easy. Have each possible value reference one of the spritelayout blocks and that's it. The default reference is of course used for view with index 0 (the first view):
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_view, view) { 1: spritelayout_fingerpost_3_SW; 2: spritelayout_fingerpost_3_NW; 3: spritelayout_fingerpost_3_NE; spritelayout_fingerpost_3_SE; }
With this the graphics for a simple object are done. If you want you can now encode this as a NewGRF.
Total code so far
If you put everything in the correct order, you should have this (no changes to the language file):
// define the newgrf grf { grfid: "\FB\FB\05\01"; name: string(STR_GRF_NAME); desc: string(STR_GRF_DESCRIPTION); version: 0; min_compatible_version: 0; } //templates template template_fingerpost(x,y,filename) { [x, y, 20, 32, -10, -28, filename] [x+30, y, 20, 32, -10, -28, filename] [x+60, y, 20, 32, -10, -28, filename] [x+90, y, 20, 32, -10, -28, filename] } //spriteset with four directions spriteset (spriteset_fingerpost_3) { template_fingerpost(0,0,"gfx/dutch_fingerpost.png") } /* spritelayouts */ //south east spritelayout spritelayout_fingerpost_3_SE { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(0); xextent: 4; yextent: 4; zextent: 24; xoffset: 6; //from NE edge yoffset: 12; //from NW edge zoffset: 0; } } //south west spritelayout spritelayout_fingerpost_3_SW { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(1); xextent: 4; yextent: 4; zextent: 24; xoffset: 12; //from NE edge yoffset: 6; //from NW edge zoffset: 0; } } //north west spritelayout spritelayout_fingerpost_3_NW { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(2); xextent: 4; yextent: 4; zextent: 24; xoffset: 6; //from NE edge yoffset: 0; //from NW edge zoffset: 0; } } //north east spritelayout spritelayout_fingerpost_3_NE { ground { sprite: GROUNDSPRITE_NORMAL; } building { sprite: spriteset_fingerpost_3(3); xextent: 4; yextent: 4; zextent: 24; xoffset: 0; //from NE edge yoffset: 6; //from NW edge zoffset: 0; } } //decide spritelayout for each of the 4 views switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_view, view) { 1: spritelayout_fingerpost_3_SW; 2: spritelayout_fingerpost_3_NW; 3: spritelayout_fingerpost_3_NE; spritelayout_fingerpost_3_SE; } item (FEAT_OBJECTS, fingerpost_3) { property { class: "NLRF"; classname: string(STR_NLRF); name: string(STR_HANDWIJZER_3); climates_available: ALL_CLIMATES; size: [1,1]; build_cost_multiplier: 2; remove_cost_multiplier: 8; introduction_date: date(1961,1,1); end_of_life_date: 0xFFFFFFFF; object_flags: bitmask(OBJ_FLAG_REMOVE_IS_INCOME, OBJ_FLAG_NO_FOUNDATIONS, OBJ_FLAG_ALLOW_BRIDGE); height: 2; num_views: 4; } graphics { default: switch_fingerpost_3_view; additional_text: string(STR_FINGERPOST_3_PURCHASE); } }
The next step for the example object will only work in recent versions of OpenTTD. For that we'll also add a version check to disable the NewGRF in older versions of the game and you'll learn how to do that on the next page. After that we'll pick up the object example again and add support for slopes and snow and desert tiles.
NML Tutorial: Object graphics
Flat tiles only
Not really part of this example, but you might ask yourself the question: what if I don't want to have this object on slopes? With the total code so far you can still build the object on slopes, but that will glitch heavily because a flat tile and not a sloped tile is drawn. If you only want to allow the object on slopes and not continue the rest of the example object, you must add the tile_check
callback. While in this callback, bits 0..3 of the extra_callback_info1
variable will contain a bitmask of the tile slope. If this bitmask is 0, the tile is flat and you can return the constant that allows object construction. In other cases return a custom or builtin error message string. This can be done directly from the graphics block using a conditional assignment. The other bits of extra_callback_info1
must be and-masked out.
For example, add this to the graphics block to only allow building on flat tiles:
tile_check: ((extra_callback_info1 & bitmask(0,1,2,3,4)) == 0) ? CB_RESULT_LOCATION_ALLOW : CB_RESULT_LOCATION_DISALLOW;