Show full frameset for easy navigation
Object animation tutorial
Unlike vehicles or stations, (new) objects have no real equivalent in original TTD. Objects in TTDPatch and OTTD are just "eye candy", so to speak. But even with the lack of game relevance, they might fill a gap in the game, in particular for the "realistic" player, especially when being used alongside game-relevant features like stations and/or town buildings. E.g., the Maritime Collection (MariCo) offers a broad set of objects visualizing harbour infrastructure like lighthouses, moles, piers, harbour buildings and loading appliances, etc.
Be aware that following examples don't result into 100% complete newGRFs, but instead concentrate on the most important code features rather than implementing it in detail and giving correct object-IDs or function references.
The first example will demonstrate a very basic animation which would be constantly looping and does not use any triggers. It is based on the mole lights from the MariCo set. The object in question would use its animation to generate a "character" (to help the sailors to discern this very light from others). This is achieved by splitting the (animated) "light" from the base object (the "lighthouse" in the further course) and thus showing two different objects in a continuous loop, either only the lighthouse or the combination of lighthouse and light.
Defining the object
First thing to do would be to define the object at all. In m4nfo, this is done by use of function defineobject(), and here it is:
defineobject(_LIGHTS, classid(MC01) classname(moles) objectname(molelight) climate(TEMPERATE, ARCTIC, TROPIC) size(1,1) price(80) timeframe(1.1.1880 .. 1.1.2050) flags(NOBUILDONLAND, HASANIMATION) callbacks(CB_TEXT) anim_info(10, LOOP) anim_speed(5) anim_triggers(BUILT) buildingheight(2) numviews(4) )
As can be seen from the function body, this object's ID is "_LIGHTS" (some number defined previously), its class-ID is set to "MC01" (to include it properly in the correct class inside the object building menu), its classname is set to the string referenced by the text identifier "moles", and its objectname to the string referenced by "molelight" (both definitions left out in this tutorial, see the user manual).
This object will be available in temperate, arctic and sub-tropical climates, it's of size 1*1, it may be built for a certain price inside a timeframe from january 1st, 1880 to january 1st 2050, must be built on water, has animation, and uses the "additional text" callback.
With regards to animation, it uses 10 animation frames in a loop, with animation speed of "5", and the only animation trigger would be the building of this particular object.
Other than this, it has a height of "2" (i.e. 2 * 8px), and comes in 4 "views", i.e. 4 different graphical representations, to be chosen when being built.
Now, some real (graphics) sprites have to be defined, this is done in m4nfo in the following way:
spriteblock(
...
// 8 lighthouses green (LIGREEN[1 .. 8])
set(
sprite(mole.pcx 10 175 09 22 8 -1 -16)
)
set(
sprite(mole.pcx 20 175 09 26 6 0 -20)
)
set(
sprite(mole.pcx 28 175 09 27 8 -1 -21)
)
set(
sprite(mole.pcx 38 175 09 25 12 -3 -19)
)
set(
sprite(mole.pcx 52 175 09 24 10 -2 -18)
)
set(
sprite(mole.pcx 64 175 09 23 4 1 -17)
)
set(
sprite(mole.pcx 70 175 09 27 8 -1 -21)
)
set(
sprite(mole.pcx 80 175 09 23 6 0 -17)
)
...
// animated green light (ANIMGREEN)
set(
sprite(mole.pcx 171 175 09 3 2 0 0) // green
)
...
)
As can be seen, we're going to use 8 different lighthouse sprites. Please note that these aren't the "views", but this code will randomly display those 8 different lights per view.
In addition, we need a green light (ANIMGREEN) for the animation. And exactly that's what the whole procedure is for: TTD has no animated green colour!
Including the graphics sprites
Now, the needed sprites having been defined in a "spriteblock" function, we need means to compose the object from its individual sprites into a "tile". This is done by function "spriteset":
//------------------------------------------------------------------ // sprite sets with 8 lighthouses green //------------------------------------------------------------------ // #1 def(50) spriteset( set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light ) def(51) spriteset( set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light set(normal(ANIMGREEN), xyoff(3,2)) // animated light (green) ) def(22) anim_frame( ref(50) if(4, 9) // dark ref(51) else )
This is the code for one (#1) of the 8 objects, they're different with regards to the sprite for the light and the animation sequence.
In m4nfo, assembling sprites into tiles is done by function "spriteset" which supports all the subtle ways a tile might be defined in TTD, e.g. sprites defining their own 3D "bounding box", or so-called "child sprites", sharing their parents bounding box, recoloured or transparent sprites, etc.
At first, let's take a look on the un-animated light (the "base"):
def(50) spriteset( set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light )
First of all, note the function "def()". Unlike other "high-level" languages, m4nfo handles plain nfo's "c-IDs" (by which the chain of control is set up) manually. In this way, the introduction of an inefficient garbage collection is avoided, and the user may benefit from "re-usage of c-IDs" which is a very handy feature in plain nfo, especially because of the existing limit of 255 c-IDs. (O/c, it's always possible to "label" the defs in m4nfo, and re-use them at a later time by that label. OTOH, using plain numbers is especially clear in a local context, rather than using endlessly long identifiers ...)
So, let's take a look on that "def(50)". It defines a spriteset consisting of two "sets", both defining a so-called "parent sprite". The first parent sprite (the mole where the lighthouse will be placed on) uses TTD's "water" sprite as its groundtile and real sprite number "2" (the mole piece) from the spriteblock above for its building tile. It defines a bounding box of 16 * 16 pixels with a height of 6 pixels, which completely covers the ground tile, because its origin is placed at coordinate (0,0), see picture below.
The second sprite is the lighthouse to be placed on top of the mole piece, using sprite LIGREEN1 from the sprite block above. This set defines a smaller bounding box of 8 * 8 pixels, located at (1,5) at a height of 6 pixels, i.e. right on top of the first bounding box.
Both sprites are handled "normally", that's why the function "normal()" is used for them. (There are special ways to handle real sprites in TTD/m4nfo, e.g. using company colour, two company colours or recolouring, those would be handled by different functions than "normal()".)
Well, that's the lighthouse tile now, consisting of a water ground sprite, a mole piece, and a lighthouse building.
Now, we need the same thing with an additional green light, and here it is:
def(51) spriteset( set(normal(WATER), normal(2), xyz(0,0,0), dxdydz(16,16,6)) // mole set(normal(LIGREEN1), xyz(1,5,6), dxdydz(8,8,16)) // light set(normal(ANIMGREEN), xyoff(3,2)) // animated light (green) )
First two sets are identical to those from def(50), but there's now a third one. This one is a so-called "child sprite", i.e. it has no bounding box, but it does share the bounding box of the previos sprite, i.e. that of the lighthouse. The xyoff() parameter specifies the offset of sprite ANIMGREEN with regards to LIGREEN1: there's an offset in x of 3 pixels, and an offset in y (height) of 2 pixels.
The deep reason to split up the sprites in this particular way lies with the problem of displaying the tile in 4 directions (x front, x back, y front, y back) which would need four times the number of sprites, while we're able now to "shift" the light sprite with regards to the mole sprite according to the direction of the tile, just by setting the xyz() coordinates in a clever way.
Well, let's move on now to the animation step. This is simply done by
def(22) anim_frame( ref(50) if(4, 9) // dark ref(51) else )
Are you still remembering that the animation was defined for 10 frames? Well, function anim_frame() references def(50) on the 4th and the 9th frame, and def(51) for all the other frames, generating the neat effect of switching off the light on frames 4 and 9.
Needless to say that the other 7 light versions are using different "characters" (animation sequences), helping the sailors to keep the lights apart.
So, in a next step, we need to randomize those 8 light versions:
def(14) randomrel(CONSTRUCT, 1, ref(22), ref(23), ref(24), ref(25), ref(26), ref(27), ref(28), ref(29)) // green
This function defines yet another def(14) randomizing defs 22, 23, ..., 29 on construction, i.e. after being built, they'd stay the same. (Objects don't have any more random triggers.)
Unfortunately, now it gets a bit lengthy, because we need those three other directions (and then, we'd need everything again for the 8 red lights, doh!), but we'll skip it here, because it doesn't introduce anything new, except from different sprites and x/y-offsets.
Anyway, eventually we're ending at this part of the code:
//------------------------------------------------------------------ // mole straight in x with lighthouse green //------------------------------------------------------------------ // mole in x, find end of straight line def(40) objinfo_water(pos(-1,0), // back ref(14) if(ENDMOLE) // lighthouse ref(0) else ) def(41) objinfo_water(pos(1,0), // front ref(16) if(ENDMOLE) // lighthouse ref(40) else ) def(42) objinfo_slope( ref(10) if(WEST+SOUTH) // slope back ref(12) if(NORTH+EAST) // slope front ref(41) else // flat, check for end tile ) def(50) callback( animcontrol(0) if(CB_ACONTROL) // start animation ref(42) else )
Remember, nfo (and m4nfo) goes backwards from its "action3" (m4nfo: "makeobject()" function), so let's take a look first on def(50). Its function "callback()" checks for callback CB_ACONTROL (animation control) active, and in case starts the animation at frame 0. In case, it's not a CB_ACONTROL, the function branches off to def(42).
Now, function def(42) checks for a potential slope (e.g., when building this object on a coast tile). Because this part of the code is only concerned with x-direction, it's sufficient to check for one back (WEST+SOUTH) or one front slope (NORTH+EAST). On such slopes, no mole lights can be built, so these branches are left for special mole building on coast tiles (defs 10 and 11).
So, only if there's no back or front slope tile, we carry on with def(41). Fortunately, we don't have to check for a flat (water or land) tile, because we can only built moles on water. So, next thing to check would be for a mole's free end (i.e., a clean water tile), because we'd like to allow the building of a mole light only at the mole's end. This is done by checking the neighbour tiles in front and back of the mole piece for being "water" by using function "objinfo_water()": We first check for a possible water tile in front of the mole piece ("pos(1,0)"), and then at the back ("pos(-1,0)"). Depending on that direction (+1 in x == front, or -1 in x == back) we're branching off to the animated mole light, i.e. def(14) from above gets references for the back direction.
If no water tile is found ("ref(0)"), a normal straight mole piece in x-direction is being built.
Now, again we have left out three other possibilities, namely green light in y-direction and red lights in x- and y-direction. Imagine, these got defs 51, 52, and 53, we now come to an end of the whole exercise:
//------------------------------------------------------------------ // make 4 "views" (green light in x and y, red light in x and y) //------------------------------------------------------------------ // graphics def(60) getviews( ref(50) if(0) // green light in x (this is our test case) ref(51) if(1) // green light in y ref(52) if(2) // red light in x ref(53) else // red light in y ) // menu def(61) getviews( ref(36) if(0) // green light in x ref(37) if(1) // green light in y ref(38) if(2) // red light in x ref(39) else // red light in y ) def(62) callback( reftxtcb(warn_water) if(CB_TEXT) ref(61) else ) makeobject(_LIGHTS, link(ref(62),MENU) default(ref(60)) )
First of all, def(60) sets up 4 "views" which can be chosen from the building menu. def(50) is the green light in x which we have come along with today, and obviously, the other three variants are linked to the remaining three views.
Now, the same is done with the menu sprites, which had been set up elsewhere (defs 36 .. 39).
So, let's finish the object: When in the building menu (MENU), control is transfered to def(62) checking for an additional CB_TEXT callback (displaying some help text) or to the menu sprites; and if not in the building menu, control is handed directly to def(60).
Yeah, this was the first animation tutorial, an easy one. Congratulations you came through! Soon, we'll moving on to something more challenging. Stay tuned.