NMLTutorial/Object graphics

From TTWiki
Jump to navigationJump to search

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 Hirundo. 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.

Dutch three-way fingerposts in four rotations. Drawn by FooBar.

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, item_fingerpost_3) {
    property {
        class:                  "NLRF";
        classname:              string(STR_NLRF);
        name:                   string(STR_FINGERPOST_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;