Difference between revisions of "NMLTutorial/Object slopes"
m (next part is now 32 bit sprites) |
Planetmaker (talk | contribs) m (off by one) |
||
(One intermediate revision by the same user not shown) | |||
Line 3: | Line 3: | ||
− | This continues and concludes the [[NMLTutorial/Object graphics|second part]] of the object example. In this last part we'll make the object compatible with sloped terrain as well as snow and desert terrain. For this some recent features of OpenTTD will be used, which makes this last part of the example '''incompatible with |
+ | This continues and concludes the [[NMLTutorial/Object graphics|second part]] of the object example. In this last part we'll make the object compatible with sloped terrain as well as snow and desert terrain. For this some recent features of OpenTTD will be used, which makes this last part of the example '''incompatible with OpenTTD earlier than 1.2.0 (r22723)'''. |
Latest revision as of 13:17, 24 May 2013
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 and concludes the second part of the object example. In this last part we'll make the object compatible with sloped terrain as well as snow and desert terrain. For this some recent features of OpenTTD will be used, which makes this last part of the example incompatible with OpenTTD earlier than 1.2.0 (r22723).
Version check
As indicated, the particular code on this page only works on OpenTTD 1.2.0 (r22723) or higher. Therefore, we want to disable this NewGRF on older versions. This is done by comparing said version number with the openttd_version
variable in an if statement. To avoid having to write the version number in hex, we can use the version_openttd()
function and have NML do that for us. If the current game version is older than said version number generate a fatal error message, otherwise skip the error message and continue with the rest of the NewGRF:
if (version_openttd(1,2,0,22723) > openttd_version) { error(FATAL, REQUIRES_OPENTTD, string(STR_OPENTTD_VERSION)); }
For the error message we set that it must be FATAL
, which means that an error message is issued and loading the NewGRF is aborted. As message we use the builtin string REQUIRES_OPENTTD
(automatically translated by the game). This default string must be supplied with the actual version number, provided via the custom string string(STR_OPENTTD_VERSION)
.
Due to this custom string reference, we of course also must define it in the language file (it's sufficient to only have this in the default language file):
STR_OPENTTD_VERSION :1.2.0 (r22723)
Sloped ground sprites
So far our object can only show flat ground tiles. Now we could make a spritelayout for each different slope (19) and each different view of the object (4) which would give us 76 spritelayouts. Luckily, recent versions of OpenTTD allow us to use temporary variable storage to be used inside spritelayouts, which means we can write a small piece of NML code that makes the game calculate what ground sprite to use for a given slope.
This calculation is done in a switch block, so we need to reference a different switch block from the graphics block. This new switch block in turn will reference the switch block we already have:
default: switch_fingerpost_3_object;
The new switch block will make the calculation based on the tile_slope
object variable using the slope_to_sprite_offset()
function. This calculates how many sprites (the offset) after the flat ground sprite the sprite for the slope is located at. This is then stored in temporary storage register 0 using the STORE_TEMP()
function.
Because we don't actually need to make a decision in this switch (it's just used to store the slope sprite offset), we only have a default value inside this switch referencing the original switch block that selected the proper spritelayout block depending on the object view:
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_object, STORE_TEMP(slope_to_sprite_offset(tile_slope), 0)) { switch_fingerpost_3_view; }
Now that we have the calculation, we must actually use it in the four spritelayout blocks for the ground sprite. This means that instead of only referencing the flat ground sprite, we must add the calculated slope sprite offset to this to actually get the sprite number for the slope we want:
//south east spritelayout spritelayout_fingerpost_3_SE { ground { sprite: GROUNDSPRITE_NORMAL + LOAD_TEMP(0); } building { sprite: spriteset_fingerpost_3(0); xextent: 4; yextent: 4; zextent: 24; xoffset: 6; //from NE edge yoffset: 12; //from NW edge zoffset: 0; } }
Here we take the sprite number of the flat ground sprite (GROUNDSPRITE_NORMAL
) and add the calculated offset to this. We get this offset from the temporary storage using the LOAD_TEMP()
function. The argument 0
defines what storage register we want to get a value from. Because we stored in 0, we need to load from 0 as well.
The ground sprite for the other spritelayouts is changed in the same way. We will not give you the other spritelayout blocks here, as they need to be changed once more.
Now we have a small problem with the purchase menu. The tile_slope
object variable isn't available there, so we cannot use the calculation for the purchase menu! This can be solved easily by not doing the calculation for the purchase menu and always have a sprite offset of 0 there (always giving the flat ground sprite). So for the purchase menu we just set the temporary storage value to 0.
First reference the purchase callback from the graphics block and link to a switch block where we will set the temporary storage:
purchase: switch_fingerpost_3_purchase;
The switch block itself will only set temporary storage register 0 to a value of 0 and then immediately reference the switch block that makes the decision based on the object views:
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_purchase, STORE_TEMP(0, 0)) { switch_fingerpost_3_view; }
Now the purchase menu works again.
Snow and desert tiles
Now our object works fine on slopes, but still shows a grass tile on snow and desert tiles. Not good! This is because we always use GROUNDSPRITE_NORMAL
as a base for the ground sprite to draw, which is the grass tile in all climates.
The solution here is to use the terrain_type
object variable to check what terrain we're actually building on. Now you can again have a bunch of spritelayouts for each different terrain type, but also here we can use the temporary storage to save what terrain sprite to use and retrieve this again in the spritelayouts. Let's give that a go, shall we?
- For this we will use the second storage register (with index 1).
- The default case is the normal grounds sprite, so store that in the register:
STORE_TEMP(GROUNDSPRITE_NORMAL, 1)
. - In case of the tropic climate, we need to choose between grass and desert depending on the
terrain_type
variable. If grass, use the storage we already had, otherwise change the storage:STORE_TEMP(terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : LOAD_TEMP(1), 1)
. What this does is store in register 1: if the terrain is desert the sprite number of the desert flat ground sprite fromGROUNDSPRITE_DESERT
and if the terrain is not desert what we already had in register 1. - In case of the arctic climate, we need to choose between grass and snow depending on the
terrain_type
variable. If grass, use the storage we already had, otherwise change the storage:STORE_TEMP(terrain_type == TILETYPE_SNOW ? GROUNDSPRITE_SNOW : LOAD_TEMP(1), 1)
. What this does is store in register 1: if the terrain is snow the sprite number of the snow flat ground sprite fromGROUNDSPRITE_SNOW
and if the terrain is not snow what we already had in register 1.
These three expressions need to be put somewhere in our NML file. Each expression can go in a separate switch block all linked together, but luckily we may provide an array of expressions in a single switch block. This is done by separating each command by a comma and grouping them together between straight brackets. The decision of the switch block is based on the last expression in the chain (but that is in this case not important as we only have a default return for the switch block).
Change the switch block we made earlier on this page:
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_object, [ //tile slope offset in storage register 0 STORE_TEMP(slope_to_sprite_offset(tile_slope), 0), //terrain type in storage register 1 STORE_TEMP(GROUNDSPRITE_NORMAL, 1), STORE_TEMP(terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : LOAD_TEMP(1), 1), STORE_TEMP(terrain_type == TILETYPE_SNOW ? GROUNDSPRITE_SNOW : LOAD_TEMP(1), 1) ]) { switch_fingerpost_3_view; }
For this new calculation to work, we must also change the spritelayout blocks. The choice of the flat ground sprite number is now stored in temporary storage register 1, so we must use this instead of the hardcoded GROUNDSPRITE_NORMAL
. Just replace GROUNDSPRITE_NORMAL
for the ground sprite with LOAD_TEMP(1)
in all spritelayout blocks. If you haven't made the first change to all four spritelayout blocks, do that now all in one go:
//south east spritelayout spritelayout_fingerpost_3_SE { ground { sprite: LOAD_TEMP(0) + LOAD_TEMP(1); } 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: LOAD_TEMP(0) + LOAD_TEMP(1); } 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: LOAD_TEMP(0) + LOAD_TEMP(1); } 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: LOAD_TEMP(0) + LOAD_TEMP(1); } building { sprite: spriteset_fingerpost_3(3); xextent: 4; yextent: 4; zextent: 24; xoffset: 0; //from NE edge yoffset: 6; //from NW edge zoffset: 0; } }
Now we have again broken the purchase menu, because the terrain_type
object variable is also not available there. This can be solved easily by again not doing the calculation for the purchase menu and always have the grass terrain sprite in the purchase menu. So for the purchase menu we just set the temporary storage register 1 value to GROUNDSPRITE_NORMAL
.
This change is similar to the change of the other switch block we just did:
switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_purchase, [ //use flat gound sprite for purchase menu STORE_TEMP(0, 0), //use normal terrain for purchase menu STORE_TEMP(GROUNDSPRITE_NORMAL, 1), ]) { switch_fingerpost_3_view; }
So for the purchase menu we just use the flat sprite of the grass terrain. Now the purchase menu works again.
With this the end of the object example is reached. You can now encode this as a NewGRF.
The complete code
If you put everything in the correct order, this will be the complete NML 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; } //check OpenTTD version //parameterized spritelayout is only supported since OpenTTD 1.2.0 r22723 if (version_openttd(1,2,0,22723) > openttd_version) { error(FATAL, REQUIRES_OPENTTD, string(STR_OPENTTD_VERSION)); } //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: LOAD_TEMP(0) + LOAD_TEMP(1); } 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: LOAD_TEMP(0) + LOAD_TEMP(1); } 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: LOAD_TEMP(0) + LOAD_TEMP(1); } 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: LOAD_TEMP(0) + LOAD_TEMP(1); } 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; } //calculate ground sprite for object switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_object, [ //tile slope offset in storage register 0 STORE_TEMP(slope_to_sprite_offset(tile_slope), 0), //terrain type in storage register 1 STORE_TEMP(GROUNDSPRITE_NORMAL, 1), STORE_TEMP(terrain_type == TILETYPE_DESERT ? GROUNDSPRITE_DESERT : LOAD_TEMP(1), 1), STORE_TEMP(terrain_type == TILETYPE_SNOW ? GROUNDSPRITE_SNOW : LOAD_TEMP(1), 1) ]) { switch_fingerpost_3_view; } //calculate ground sprite for purchase menu switch (FEAT_OBJECTS, SELF, switch_fingerpost_3_purchase, [ //use flat gound sprite for purchase menu STORE_TEMP(0, 0), //use normal terrain for purchase menu STORE_TEMP(GROUNDSPRITE_NORMAL, 1), ]) { switch_fingerpost_3_view; } 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_object; purchase: switch_fingerpost_3_purchase; autoslope: return(CB_RESULT_AUTOSLOPE); additional_text: string(STR_FINGERPOST_3_PURCHASE); } }
The language file will now contain:
##grflangid 0x01 #Main grf title and description STR_GRF_NAME :{TITLE} STR_GRF_DESCRIPTION :Description: {SILVER}Dutch Road Furniture is an eyecandy object NewGRF that features road furniture that can be found alongside Dutch roads. {}(c)2011 FooBar. {}{BLACK}License: {SILVER}GPLv2 or higher. #error messages STR_OPENTTD_VERSION :1.2.0 (r22723) #object classes STR_NLRF :Dutch Road Furniture #object name and description STR_FINGERPOST_3 :Dutch Fingerpost three-way STR_FINGERPOST_3_PURCHASE :The three-way fingerpost is centered at one side of the tile and facing outward. Intended to be placed directly opposite of the secondary road at a three-way junction.
The next part of the tutorial will teach you some things about adding 32 bit sprites to your NewGRFs.
NML Tutorial: Object slopes